-
Notifications
You must be signed in to change notification settings - Fork 1
Glacier: How to read decompiled PRP
This document describes how to read decompiled PRP file as JSON.
Decompiler tool is here.
JSON specification is here.
Notice:
1. Make backups
2. Be careful and do not remove any data!
3. Don't change any instruction types if you don't know what you doing.
4. Be careful when you add a script or change its name. It could crash the game in some cases.
5. Make backups
6. Do not re-create the whole ZIP file. Use windows explorer and replace PRP files when you want to check your changes.
7. MAKE BACKUPS!!!11111
In Glacier (gen. 1) the scene was divided by few files:
- GMS - game scene definition
- BUF - static buffer (data trashcan/unstructured)
- PRM - primitives (models/meshes)
- PRP - properties
So, the PRP file is a classic 'execution machine' pattern. There are 4 sections:
- Header (general information about the file)
- Token table (strings)
- ZDefines (common properties of level)
- Properties list
Each scene contains at least 2 scene definitions:
- ScriptCModule: this is the name of DLL which contains all in-game scripts
- PostFilterPalettes: array of int32. Unknown what it means.
But PRP can contain much more definitions. And you may add/change your own definitions, it will not crash the game if you do it properly.
This is the main part of the PRP file. It contains instruction bytes and each instruction can be explained as 1 byte with op-code and a few next bytes as its own data. More details about PRP is here
Be aware of instructions order, it's very important! Don't change it!
When you understand what is PRP, ZDefines & properties list you can work with PRP file in depth.
First of all, you need to decompile your first PRP. Let it be Hideout/Loader_Sequence.ZIP/Scenes/Loader_Sequence.PRP.
Use prptool.py to decompile it: python prptool.py Loader_Sequence.PRP Loader_Sequence.JSON decompile
.
Now, you are ready to open this file in your editor (Visual Studio Code, notepad, as you like).
Here you can see 4 global sections:
- is_raw - flag from the original PRP. Do not change it, it must be false always (let me know if it doesn't)
- flags - mask of original PRP file. It must be 13 (0xD). Let me know if it doesn't.
- definitions - first useful section of PRP. Here are located ZDefines from PRP.
- properties - second useful section of PRP. Here are located instructions of PRP.
So, now you are ready to go deeper into the Glacier properties system.
So, we are talking about 'PRP', 'properties', 'Z-things', but what is it? Why 'Z'? Why binary files? What are types? Let's talk about that.
Glacier, like other engines, is working with OOP objects. Everything on the scene would be represented as separated objects/group of objects.
In glacier-specific talks, we can say that everything is ZGEOM. Z - is a common prefix in the glacier code style guide. There are 3 prefixes:
- Z - Class or struct
- C - Class
- E - Enumeration
In most cases, we can see the 'Z' prefix.
So, we know that everything is ZGEOM. What's next?
So, we have a world of objects. Generally, we can represent any object as a group of properties. First of all - type of object.
In managed languages (like C#, Python, YOUR_BEST_LANGUAGE) compiler/interpreter knows what type of object has and which properties exist for it. But Glacier was written in C++ and here we have RTTI but that's not enough to make your world serializable.
So, we may write our own serialization code, write bindings but it's boring. So, Glacier makes its own RTTI compiled inside executable (via defines). And uses that to deserialize everything. Our property instructions are the result of serialization. But as we can see our PRP->JSON doesn't contain any type ids, what's wrong? Yeah, this is the first hard place: type id is stored inside the GMS file.
But we know that the first object in PRP is always ZROOM, let's talk about that.
First of all, we should remember that everything is ZGEOM. Therefore every object definition starts from PRPOpCode.BeginObject
. You may say: "What the problem? Just find every BeginObject opcode and you will have all objects on the scene" and you will not be right. Let's talk about ZGEOM structure
Every ZGEOM contain three sections:
- Properties - specific for own type properties (instructions) list
- ZEvents container - container of ZEventBase inner things
- Children container - container of children ZGEOM's
Every ZGEOM could be expanded in two ways
- Inherit new class of ZGEOM and add your own logic (old way)
- Create new ZEventBase class and add it as 'ZEvent' to any ZGEOM (new way)
The second option is more preferred and it looks like ECS (ZEventBase = Component + System)
It's just a bunch of child geoms. Nothing interesting.
Also, some types contain their own objects, and to recognize where starts new ZGEOM you need visit every instruction and save flags into your own context to know what's happening right now.
We know that the first object is always ZROOM. Let's read it!
We will use this document as help.
The first instruction PRPOpCode.BeginObject
is a marker to interpret that new objects begin.
Instruction PRPOpCode.StringOrArray_E
with op_data = "BOUNDING_Static" means that our ZGEOM has EBoundingBox::BOUNDING_Static value.
Now we can see PRPOpCode.Array
. It means that we have an array of N elements, where N is declared in the op_data
section at 'length'.
This is a 'Matrix' (not a film) of the ZGEOM object. It represents the transformation of objects in the graphics world.
And next 9 op-codes will represents all entries of your array (float32[9]).
The next array represents 'Position' data (Vector3).
The next bool flag means 'Inactive' flag of ZGEOM. And the last ZGEOM's op-code is Int32 means primitive ID (index of primitive in PRM file).
So, our ZGEOM properties ended. Now, we reading op-codes from ZGROUP. Then of ZTreeGroup and the last instructions set represents ZROOM itself.
Result:
Property | Type | Namespace | Value |
---|---|---|---|
BoundingBox | EBoundingBox | ZGEOM | BOUNDING_Static |
Matrix | float[9] | ZGEOM | (0, 0, 1, 0, 1, 0, 1, 0, 0) |
Position | float[3] | ZGEOM | 0,0,0 |
Inactive | bool | ZGEOM | false |
PrimId | int | ZGEOM | 0 |
property_40 | float | ZGROUP | 1.0 |
bLightShinesIn | bool | ZGROUP | true |
bLightShinesOut | bool | ZGROUP | true |
bIsDynamicContainer | bool | ZTreeGroup | true |
bIsPrivate | bool | ZTreeGroup | true |
bIsPrivate | bool | ZTreeGroup | false |
bIsStaticContainer | bool | ZTreeGroup | true |
property_6C | uint | ZROOM | 0 |
property_84 | int8 | ZROOM | 0 |
property_A4 | ZAUDIOREF (int) | ZROOM | 0 |
NoiseLevel | ENoiseLevel | ZROOM | eNormal |
Location | ELocation | ZROOM | eUNDEFINED |
property_12C | RawData | ZROOM | Container { length=0 } |
NotInRoomData | bool | ZROOM | false |
DisableAudiEvt | bool | ZROOM | false |
ExitOffsets | int | ZROOM | 0 |
NeighborsOffset | int | ZROOM | 0 |
EnvironmentLight | ZGEOMREF (string) | ZROOM | "" (empty string) |
property_134 | float[3] | ZROOM | [0.0, 0.0, 0.0] |
ZEvents: [] (no extra ZEventBase entries)
Children: [] (no children geoms)
Now I'm working on a GUI tool to edit PRP & GMS but before it, you may create your own mods.
The base method is to find the thing that you want to change by primitive ID or by position (via ReHitman). If you would like to find an entity by the position you should look by an integral part of the float number (because ending may change at runtime).
Remember about actors: it's implemented via 2 geoms: parent and child. Parent geom contains position but not primitive id. The child geom contains all other data.
python prptool.py MyPrpFile.JSON MyPrpFile.PRP compile