Solved Searching in World.Entities fails Silently

.phase

Member
Member
Jul 29, 2014
58
12
8
Southern California
So, this used to work in Magma, but is failing in Jint:
JavaScript:
                    if(args[0] == "barricade"){
                        for (var allObj in World.Entities){
                            if(allObj.Name == "Barricade_Fence_Deployable"){
                                Util.DestroyObject(allObj.Object.gameObject);
                            }
                        }
                    }
It fails silently, and I read @mikec post about that, tried a try/catch, Util.Log, but nothing shows up. It just fails, whereas it used to work in Magma.

Not sure what's going on.o_O
 
  • Winner
Reactions: mikec

mikec

Master Of All That I Survey
Retired Staff
Trusted Member
Jul 12, 2014
296
152
28
Los Angeles, California, USA
So, this used to work in Magma, but is failing in Jint:
JavaScript:
                    if(args[0] == "barricade"){
                        for (var allObj in World.Entities){
                            if(allObj.Name == "Barricade_Fence_Deployable"){
                                Util.DestroyObject(allObj.Object.gameObject);
                            }
                        }
                    }
It fails silently, and I read @mikec post about that, tried a try/catch, Util.Log, but nothing shows up. It just fails, whereas it used to work in Magma.

Not sure what's going on.o_O
This is one of my favorite bug reports now. :) This bug report led me down the rabbit hole into the depths of the .Net Common Language Runtime (CLR). When I looked at the code for World.Entities, I knew what the solution was going to be, but I didn't understand why the bug was in World.Entities and not in Jint2.2. Now I understand why, and it was an important lesson learned. ;)

It also give me an opportunity to talk about important differences between old Jint in Magma and new Jint2 in JintPlugin.

Thank you! :cool:

The problem in simple terms is that World.Entities is typed to return an IEnumerable which is a kind of list of Entity objects. The return value is:
C#:
return structures.Concat(deployables);
structures is a IEnumerable of structure components and deployables is a IEnumerable of deployable objects. Concat does just what it sounds like, it returns a new list with all the objects in deployables added to the end of structures.

Except that it doesn't. It actually returns an object of this type:
C#:
System.Linq.Enumerable+<CreateConcatIterator>c__Iterator1`1[Fougerite.Entity]
:eek:o_O:confused:
CreateConcatIterator is a private method in System.Linq.Enumerable which is called by the Concat method to join the two lists. c__Iterator1 is the compiler-generated name for the first of two foreach loops within CreateConcatIterator. It has no properties, no methods. It's an internal compiler-created object.

Q: Why does Jint2 return a compiler-generated object to a Javascript plugin?
A: Because the code in World Entities told it to: return structures.Concat(deployables);
Q: Does old Jint return the compiler-generated object to a Javascript plugin?
A: Yes, it sure does. World.Entities returns the same object to all callers, any language.
Q: Then why does it work in Magma and C# but not in Jint2 Javascript plugins?
A: That's an excellent question. Glad you asked.

The object - two foreach loops in CreateConcatInterator - isn't executed to produce the combined list until it is used in a foreach loop . Javascript does not have a C#-style foreach loop. But in old Jint, a Javascript for(var p in object) loop was transformed into a C# foreach loop "under the hood." Old Jint tried to allow Javascript to treat C# objects as Javascript objects.

Jint2 has a new Engine and takes a different approach. The only C# objects that appear as Javascript objects, with enumerable properties and methods (including the case of the member names) are those that are directly equivalent, or most closely equivalent when there isn't an exact match to a Javascript object. The only list-like object (class) in C# that has a direct equivalent in Javascript is Array. So a C# array in Javascript has .length property, not .Length, and a method .forEach, not .ForEach. And a C# string is a JS string with JS string methods, int is int, float is float (C# single-precision), and so on. And a Javascript for(var p in object) loops the (enumerable) properties of a Javascript object as documented. In the case of an array, the members are the indexes. If World.Entities was an array you would do this:
JavaScript:
for (var i in World.Entites) {
    if(World.Entities[i].Name == "Barricade_Fence_Deployable"){
        Util.DestroyObject(World.Entities[i].Object.gameObject);
    }
}
But it doesn't act on other C# classes like List or Dictionary. Those objects don't have enumerable properties, not for Javascript. A loop over an object with no enumerable properties will run zero times. It's not an error. No exception is thrown.
HERE IS AN IMPORTANT POINT
But even if C# list-type objects - List, Dictionary, etc. - had Javascript-enumerable properties in Jint2, the for loop you wrote wouldn't work as you expected. It would return the members of the List or Dictionary class, not the elements of the collection wrapped up inside the class. And you couldn't refer to them "bare" as you did with your var allObj. You refer to World.Entities[allObj] inside a Javascript for loop. allObj is just a string, the name of the property. The style of Javascript loop you wrote only works in old Jint.

So, old Jint has taught you bad Javascript. It's a good thing to be rid of it. And, a lot of ordinary Javascript won't run in old Jint. Things like jQuery. Jint2 can run jQuery.

The collection of elements are but one member of the class List or Dictionary, or any other C# Collection class. To get the elements the "Javascript way" you would need the name of the member that held the collection, and access it something like Listname[membername][x] where x is the index in the array that underlies a C# Collection. But this is private to the class, and you can't access it directly.

Q: Things are looking pretty dismal for Jint2. I'd rather just write Rust plugins like I did before. "For" loops are easy in Magma. I care about making cool plugins, and not so much about correct Javascript. Jint2 seems more broken than correct, anyway. Are you going to fix it?
A: You can keep writing for Magma. It will work as-is indefinitely, and we'll still fix bugs that affect it. You can start writing plugins for Jint2 that share data with Magma plugins using DataStore. That way you don't have to start all over. You can migrate to Jint2 gradually.
Q: So, what about World.Entities? Is that Magma-only? Does Jint2 have a replacement?
A: In Fougerite MC5 I "finished the job" and converted the output of the Concat method to a List<Entity>. That's what it was in the original Magma 1.1.5. Magma plugins will work exactly the same, with a foreach loop behind the scenes returning each element of the collection.
Q: So I can do a for loop on it in my Jint2 plugin?
A: No. As I said, a List has no properties enumerable in Javascript and...
Q: Got it. GOT IT. SHUT UP I GOT IT.
A: OK. But now (in MC5) that World.Entities returns a List object, instead of a code fragment that produces a List, Jint2 plugins can use the properties and methods of the List class to manipulate the List. The code fragment object had no properties Jint2 plugins can use.
Q: Um. So... and then?
A: And this:
JavaScript:
function getbarricades(x) {
    return x.Name == "Barricade_Fence_Deployable";
}
function destroy(x) {
    Util.DestroyObject(x.Object.gameObject);
}
var barricades = World.Entities.FindAll(getbarricades);
Plugin.Log("BarricadesNoMore", "Destroying " + barricades.Count + " barricades.");
barricades.ForEach(destroy);
Q: That kinda sucks.
A: Noob. But that's OK. Have it your way.
JavaScript:
var entity = World.Entities.ToArray();
for(var i in entity) {
    if(entity[i].Name == "Barricade_Fence_Deployable"){
        Util.DestroyObject(entity[i].Object.gameObject);
    }
}
Q: Ummmm.
A: JintPlugin offers some new methods for getting lists of game objects, and the lists are returned as arrays. You won't really want or need to use World.Entities soon.
 

.phase

Member
Member
Jul 29, 2014
58
12
8
Southern California
This is one of my favorite bug reports now. :) This bug report led me down the rabbit hole into the depths of the .Net Common Language Runtime (CLR). When I looked at the code for World.Entities, I knew what the solution was going to be, but I didn't understand why the bug was in World.Entities and not in Jint2.2. Now I understand why, and it was an important lesson learned. ;)

It also give me an opportunity to talk about important differences between old Jint in Magma and new Jint2 in JintPlugin.

Thank you! :cool:
When I saw an alert for a Winner rated post with no reply, I had a feeling a discovery was made, and an explanation was to follow. Thanks for the fantastic post. Some people would have been content with "Here, do it this way, noob. ;)" Being a total novice, but infinitely curious, I really do appreciate the level of depth you get into.

I started playing Rust because a friend recommended it, and peeing my pants during a raid was mad fun. Then I discovered modded servers, and helped to run a fairly popular "war/battlefield" server. It didn't take long before getting under the hood was more fun than actually playing the game. I love finding answers to "I wonder if we can..." questions.

So, again, thank you, and everyone on these forums, for entertaining a very healthy, and fun hobby of mine. :)
 
  • Agree
Reactions: Snake and mikec

Snake

Moderator
Moderator
Jul 13, 2014
288
174
28
When I saw an alert for a Winner rated post with no reply, I had a feeling a discovery was made, and an explanation was to follow. Thanks for the fantastic post. Some people would have been content with "Here, do it this way, noob. ;)" Being a total novice, but infinitely curious, I really do appreciate the level of depth you get into.

I started playing Rust because a friend recommended it, and peeing my pants during a raid was mad fun. Then I discovered modded servers, and helped to run a fairly popular "war/battlefield" server. It didn't take long before getting under the hood was more fun than actually playing the game. I love finding answers to "I wonder if we can..." questions.

So, again, thank you, and everyone on these forums, for entertaining a very healthy, and fun hobby of mine. :)
As you, it's more fun to me to discover new things that can be done than playing right now...
 

mikec

Master Of All That I Survey
Retired Staff
Trusted Member
Jul 12, 2014
296
152
28
Los Angeles, California, USA
JavaScript:
function getbarricades(x) {
    return x.Name == "Barricade_Fence_Deployable";
}
function destroy(x) {
    Util.DestroyObject(x.Object.gameObject);
}
var barricades = World.Entities.FindAll(getbarricades);
Plugin.Log("BarricadesNoMore", "Destroying " + barricades.Count + " barricades.");
barricades.ForEach(destroy);
I've tried several ways of passing a Javascript function to the List methods FindAll and ForEach, none worked. I get a type error. If I find a way to do it, I'll post here.

Meantime, just convert the List to Array and carry on using Javascript methods. This works perfectly:
JavaScript:
Plugin.Log("Entities", "World.Entities.Count: " + World.Entities.Count);
Plugin.Log("Entities", "World.Entities.ToArray().length: " + World.Entities.ToArray().length);

function getbarricades(x) {
    return x.Name == "Barricade_Fence_Deployable";
}
function logdetail(x) {
    Plugin.Log("Entities", "Owner: " + x.OwnerID + " Location: " + x.X.toFixed(1) + "," + x.Y.toFixed(1) + "," + x.Z.toFixed(1));
}
function destroy(x) {
    Util.DestroyObject(x.Object.gameObject);
}

var barricades = World.Entities.ToArray().filter(getbarricades);
Plugin.Log("Entities", "Found " + barricades.length + " barricades.");
barricades.forEach(logdetail);
barricades.forEach(destroy);
Code:
[9/16/2014 7:55 PM] World.Entities.Count: 6793
[9/16/2014 7:55 PM] World.Entities.ToArray().length: 6793
[9/16/2014 7:55 PM] Found 3 barricades.
[9/16/2014 7:55 PM] Owner: 76561198115301020 Location: 6386.3,367.0,-4408-.5
[9/16/2014 7:55 PM] Owner: 76561198054481150 Location: 4380.8,490.9,-3964-.9
[9/16/2014 7:55 PM] Owner: 76561198054481150 Location: 4379.2,491.5,-3960-.1
[9/16/2014 7:56 PM] World.Entities.Count: 6790
[9/16/2014 7:56 PM] World.Entities.ToArray().length: 6790
[9/16/2014 7:56 PM] Found 0 barricades.
 
  • Informative
Reactions: CorrosionX

mikec

Master Of All That I Survey
Retired Staff
Trusted Member
Jul 12, 2014
296
152
28
Los Angeles, California, USA
I was able to realize a significant performance boost by replacing the call to Concat() the deployable list to the structure list, with 3 calls. First to initialize a List<Entity> with the capacity to hold both, and then two calls to AddRange() to add the structure and deployable lists.
C#:
        public List<Entity> Entities
        {
            get
            {
                IEnumerable<Entity> component = from c in
                    (UnityEngine.Object.FindObjectsOfType<StructureComponent>() as StructureComponent[])
                    select new Entity(c);
                IEnumerable<Entity> deployable = from d in
                    (UnityEngine.Object.FindObjectsOfType<DeployableObject>() as DeployableObject[])
                    select new Entity(d);
                // this is much faster than Concat
                List<Entity> entities = new List<Entity>(component.Count() + deployable.Count());
                entities.AddRange(component);
                entities.AddRange(deployable);
                return entities;
            }
        }