Skip to content

Objects in script runtimes ​

A Lua table with an array part and a hash part, and the order next() visits them

Many games keep their rules in a script language: Lua, ActionScript, UnrealScript, Python, JavaScript or C# in Unity. Each language has a runtime that stores the script's objects in its own way. Two things about that storage matter to a reimplementation. The first is where a field of an object sits in memory, which you need when you read a value from a running game. The second is the order in which the runtime visits an object's fields or a table's entries, because game logic that loops over a table depends on it.

This page describes both for the runtimes games used. The facts come from each runtime's documentation and source. Engine structures covers objects in compiled C++ engines, and Memory covers finding a value in a running game.

How each runtime stores an object ​

RuntimeAn object isA named field is found by
Lua 5.xA table with an array part (keys 1 to n) and a hash partHashing the key and following a chain inside the hash part
Flash AVM2 (ActionScript 3)An object of a sealed class has fixed slots, one per declared variable; a dynamic object also has a hash tableA slot number for declared variables; a hash lookup for dynamic ones
UnrealScriptAn object of a class whose properties sit at fixed offsets, set when the class is compiledThe offset, which the class's property list in the package gives
CPythonAn object with a dictionary of attributes, or fixed slots when the class declares __slots__Hashing the name in the dictionary
JavaScript (V8, SpiderMonkey)An object with a hidden class (a shape) that maps names to slots, plus the slotsThe shape, then the slot; the runtime caches the slot at each place in the code
Mono and .NET (Unity)An object header with a pointer to its class, then the fields at offsets fixed by the classThe offset, from the class's metadata

Shapes. JavaScript engines give objects with the same fields, added in the same order, the same hidden class. The class records the slot of each field. Adding a field moves the object to another hidden class. Two objects with the same fields added in a different order have different hidden classes. In a heap snapshot from the browser's tools, each object lists its fields by name, so you can find a value without knowing the slots.

Fixed layouts. UnrealScript, Mono and sealed ActionScript classes put each field at a fixed place. Read the place from the class definition, which is the package's property list, the .NET metadata (with ILSpy or dnSpy), or the ABC file's traits (with JPEXS). Then a memory dump can be read like a C structure.

Hash tables. Lua tables, Python dictionaries and dynamic ActionScript objects store fields by hashing the name. A field's place depends on the hash of its key, the size of the table and the keys inserted before it. To read such a field from a dump, walk the table as the runtime does, or ask the running game through a debugger or a console.

The order of iteration ​

A loop over all of an object's fields, or all of a table's entries, visits them in an order the runtime chooses. If the loop does something visible for each entry, such as spawning a unit, drawing a random number or printing a line, that order is part of the game's behaviour.

RuntimeOrder of a loop over entries
Lua pairs and nextThe array part in order, then the hash part in the order of its internal nodes. The node order depends on the hash of each key, the table's size and the history of insertions. The manual leaves it unspecified
Python dicts, 3.7 and laterInsertion order, guaranteed by the language
Python dicts before 3.6Hash order, which changes with the table's size. String hashes are randomised per run from Python 3.3, unless PYTHONHASHSEED is set
JavaScript objectsInteger-like keys in ascending order, then other string keys in insertion order
ActionScript 3 for..in on dynamic propertiesNot specified; it follows the player's hash table
.NET DictionaryInsertion order while nothing is removed; a removal leaves a gap that the next insertion fills. The order is not part of the documented contract
.NET HashSet, HashtableHash order

Port the order of the entries as well as their contents. If a Lua loop over a table spawns enemies, the port must spawn them in the same order. There are two ways to do that. Port the runtime's table exactly, with its hash function, its sizes and its collision handling. Or record the order the original used for each table in each situation, and show that it never changes. The first way is more work, but it holds for every input. A reimplementation of the runtime itself, such as a port that embeds the same Lua version, gets the order for free.

Watch for orders that change from run to run. Python's string hash randomisation, tables keyed by object addresses, and hash orders that depend on memory layout can make the original itself behave differently on each run. Record that as a finding. The port can then choose one order, and the deviations ledger says which.

Reading the scripts ​

Before porting a runtime, decompile the scripts. The bytecode keeps names, and the names show what each table holds.

LanguageDecompiler
Lua 5.0 to 5.4unluac, luadec; LuaJIT has its own bytecode and tools
ActionScript 1, 2 and 3JPEXS Free Flash Decompiler (Flash and Shockwave games)
UnrealScriptUE Explorer, and the tools in Unreal Engine 1 and 2
Python bytecodedecompyle3, uncompyle6, pycdc; each covers some Python versions
C# in UnityILSpy, dnSpy; for IL2CPP builds, Il2CppDumper first (Engine-era PC games)

What to record ​

  • The runtime and its exact version.
  • For each table or object type the rules loop over, the order of the loop and how the port reproduces it.
  • Any order that changes between runs of the original.

Code: Apache 2.0. Content: CC0 1.0. No game material: bring your own copy.