forked from RyanJGray/AlienIsolation.DevTools
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathLIVE_LINK.cpp
More file actions
2856 lines (2694 loc) · 121 KB
/
Copy pathLIVE_LINK.cpp
File metadata and controls
2856 lines (2694 loc) · 121 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#include "LIVE_LINK.h"
#include "LIVE_LINK_SERVER.h"
#include "LIVE_CAMERA.h"
#include "LIVE_ANIMATION.h"
#include "LIVE_TRACE.h"
#include "DEBUG_MARKER.h"
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
#include <DirectXMath.h>
#include <cmath>
#include <cstdio>
#include <algorithm>
#include <array>
#include <cstring>
#include <iterator>
#include <limits>
#include <map>
#include <set>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <vector>
using namespace LIVE_LINK;
/*
Addresses are RVAs in the Steam retail AI.exe. Calling conventions follow what the game's own call
sites do: a class returned by value comes back through a hidden pointer passed first, and a counted pointer passed by
value is an allocation the callee releases.
*/
namespace
{
uintptr_t Address(uintptr_t rva) { return DEVTOOLS_RELATIVE_ADDRESS(rva); }
// ---- Game globals ----
uint8_t* EntityManager() { return *reinterpret_cast<uint8_t**>(Address(0x0134ef40)); } // the entity manager
uintptr_t PackFileBase() { return *reinterpret_cast<uintptr_t*>(Address(0x0134ef44)); } // the loaded COMMANDS.PAK, which packed offsets count from
void* StringTable() { return *reinterpret_cast<void**>(Address(0x0134ef78)); } // the string table
// The entity manager's fields
constexpr uint32_t kManagerRootGuid = 0x0C; // the running level's root composite id
constexpr uint32_t kManagerTemplates = 0x18; // the level's composite templates (an array of packed pointers)
constexpr uint32_t kManagerRoot = 0x44; // the root entity (the level's top instance)
constexpr uint32_t kManagerInitialised = 0x105; // set once the entity manager has initialised
constexpr uint32_t kManagerShuttingDown = 0x106;
constexpr uint32_t kManagerOffline = 0x108; // when set, an instance that is building its entities keeps every one
constexpr uint32_t kManagerTransport = 0xBC; // the script transport: 1 paused, 2 pause menu, 5 running
constexpr uint32_t kTransportPaused = 1, kTransportPauseMenu = 2, kTransportRunning = 5;
constexpr ULONGLONG kTransportSettleMs = 5000; // how long the transport must stay running before edits are taken
// The transport as last seen on the entity thread, since when, and the last value it had that was not running (what
// the level's scripts are coming back from)
uint32_t g_transport = ~0u;
ULONGLONG g_transportSince = 0;
uint32_t g_transportStopped = kTransportPaused;
// Whether this level's scripts have run for the whole settle time since it loaded: a pause after that is a cutscene or
// a message on screen waiting for the player, not the level starting
bool g_levelPlayed = false;
// An entity's fields: its vtable, its state flags, its owner, then its id and its type
constexpr uint32_t kEntityState = 0x04;
constexpr uint32_t kEntityGuid = 0x0C;
constexpr uint32_t kEntityType = 0x10;
// An instance's fields
constexpr uintptr_t kCompositeInstanceVTable = 0x00fe1d2c; // the vtable an instance object starts with (its primary one)
constexpr uint32_t kInstanceEntities = 0x2C; // the instance's entities (an array)
constexpr uint32_t kInstanceTemplate = 0x30; // the composite template the instance was made from (a packed pointer)
// An entity's virtual slots (byte offsets into its primary vtable)
constexpr uint32_t kVFindEntity = 0x0C; // an instance's lookup of an entity by id: its children, then its aliases
constexpr uint32_t kVRelease = 0x28; // releases the entity
constexpr uint32_t kVRejected = 0x48; // when this says yes, an instance building its entities releases the entity
constexpr uint32_t kVGetInterface = 0x70; // gives the entity's interface
constexpr uint32_t kVAddEntity = 0x94; // adds a child to an instance (given the instance, then the child)
constexpr uint32_t kVRemoveEntity = 0x98; // removes a child from an instance (given the instance, then the child)
constexpr uint32_t kVPrimaryZone = 0xD0; // gives the entity's primary zone
constexpr uint32_t kVSecondaryZone = 0xD4; // gives the entity's secondary zone
constexpr uint32_t kVIsTemplated = 0x128; // whether the entity was made from a template
constexpr uint32_t kVMarkedForDelete = 0x12C;// whether the entity is marked for delete
constexpr uint32_t kVRequiresScript = 0x130; // whether the entity needs scripting
// The entity interface's virtual slots
constexpr uint32_t kVFlushCache = 0x2FC; // drops the entity's cached parameters and links
// Flags for the record the game makes to go with a call into an entity, as its own callers set them
constexpr uint32_t kInfoLifecycle = 0x04000000; // an instance initialising / shutting down its entities
constexpr uint32_t kInfoLiveEdit = 0x20000000; // a composite template being edited live (as the dev build does)
// The game's counted allocation: for a single item, data is the object; for an array, count and data are the array.
struct Allocation
{
void** vtable;
volatile LONG references;
uint32_t count;
void* data;
};
// ---- Game functions ----
typedef void(__thiscall* t_memory_ptr_dtor)(Allocation** self);
auto entity_ptr_dtor = reinterpret_cast<t_memory_ptr_dtor>(Address(0x00198230)); // releases a counted reference to an entity
auto zone_ptr_dtor = reinterpret_cast<t_memory_ptr_dtor>(Address(0x00005770)); // releases a counted reference to a zone
typedef Allocation** (__thiscall* t_construct_entity)(void* manager, Allocation** result, Allocation* owner, uint32_t guid, uint32_t type);
auto construct_entity = reinterpret_cast<t_construct_entity>(Address(0x00567b30));
typedef Allocation** (__thiscall* t_construct_proxy)(void* manager, Allocation** result, Allocation* owner, const void* proxy);
auto construct_proxy = reinterpret_cast<t_construct_proxy>(Address(0x00522fc0));
typedef void** (__thiscall* t_create_info)(void* manager, void** result, Allocation** entity, uint32_t flags, uint32_t unused, const void* data);
auto create_info = reinterpret_cast<t_create_info>(Address(0x00526ff0));
typedef void(__thiscall* t_remove_reference)(void* info);
auto remove_reference = reinterpret_cast<t_remove_reference>(Address(0x005252f0));
typedef void(__thiscall* t_pause_context)(void* manager, Allocation** entity);
auto requires_push_pause_context = reinterpret_cast<t_pause_context>(Address(0x0052d310));
auto requires_pop_pause_context = reinterpret_cast<t_pause_context>(Address(0x0052f0e0));
typedef bool(__thiscall* t_state_call)(void* state, Allocation** entity, void** info);
auto state_initialise = reinterpret_cast<t_state_call>(Address(0x0053ac50));
auto state_validate = reinterpret_cast<t_state_call>(Address(0x00536850));
auto state_revert = reinterpret_cast<t_state_call>(Address(0x0053a9d0));
auto state_shutdown = reinterpret_cast<t_state_call>(Address(0x0053aad0));
auto state_live_edit = reinterpret_cast<t_state_call>(Address(0x0053b0c0));
typedef void(__thiscall* t_zone_entity)(void* zone, Allocation** zonePtr, Allocation** entity);
auto add_entity_to_zone = reinterpret_cast<t_zone_entity>(Address(0x005478a0));
auto remove_entity_from_zone = reinterpret_cast<t_zone_entity>(Address(0x00548330));
// Calls a method on an entity through a temporary entity: queues the call by adding a trigger to the entity manager
typedef void(__thiscall* t_call_custom_method)(void* temporaryEntity, Allocation** entity, const uint32_t* method, uint32_t flags);
auto call_custom_method = reinterpret_cast<t_call_custom_method>(Address(0x005425a0));
constexpr uint32_t kTemporaryEntityTarget = 0x24; // the entity pointer the queued call builds its trigger from
typedef void(__thiscall* t_access)(void* access);
auto access_lock = reinterpret_cast<t_access>(Address(0x0051e1b0)); // takes the entity manager's access lock
auto access_unlock = reinterpret_cast<t_access>(Address(0x0051e1d0)); // gives the entity manager's access lock back
typedef void(__thiscall* t_add_to_string_table)(const uint32_t* string);
auto add_to_string_table = reinterpret_cast<t_add_to_string_table>(Address(0x0051df70));
typedef const char* (__thiscall* t_string_from_offset)(void* table, uint32_t offset);
auto string_from_offset = reinterpret_cast<t_string_from_offset>(Address(0x00532160));
// Variable types, as the entity manager turns a parameter's type guid into its vtable when it loads COMMANDS.PAK
enum class VariableKind { Bool, Int, Float, String, FilePath, SplineData, Direction, Position, Enum, ShortGuid, Unknown };
struct VariableType { VariableKind kind; uintptr_t guidRva; uintptr_t vtableRva; const char* name; };
const VariableType kVariableTypes[] = {
{ VariableKind::Bool, 0x0134ba70, 0x00fc451c, "bool" },
{ VariableKind::Int, 0x0134ba74, 0x00fc45bc, "int" },
{ VariableKind::Float, 0x0134ba78, 0x00fc465c, "float" },
{ VariableKind::String, 0x0134ba7c, 0x00fc46fc, "String" },
{ VariableKind::FilePath, 0x0134ba80, 0x00fc479c, "FilePath" },
{ VariableKind::SplineData, 0x0134ba84, 0x00fc4da4, "SplineData" },
{ VariableKind::Direction, 0x0134ba88, 0x00fc497c, "Direction" },
{ VariableKind::Position, 0x0134ba8c, 0x00fc4a1c, "Position" },
{ VariableKind::Enum, 0x0134ba90, 0x00fc483c, "Enum" },
{ VariableKind::ShortGuid, 0x0134ba94, 0x00fc48dc, "ShortGuid" },
};
// ---- Helpers ----
template<typename T> T* Packed(uint32_t offset)
{
if (offset == 0xFFFFFFFF)
return nullptr;
return reinterpret_cast<T*>(static_cast<uintptr_t>(PackFileBase() + offset * 4u));
}
void* Object(Allocation* allocation) { return allocation ? allocation->data : nullptr; }
template<typename F> F Virtual(void* object, uint32_t byteOffset)
{
return reinterpret_cast<F>((*reinterpret_cast<void***>(object))[byteOffset / 4]);
}
uint32_t EntityGuid(Allocation* entity) { return *reinterpret_cast<uint32_t*>(static_cast<uint8_t*>(Object(entity)) + kEntityGuid); }
uint32_t EntityType(Allocation* entity) { return *reinterpret_cast<uint32_t*>(static_cast<uint8_t*>(Object(entity)) + kEntityType); }
void* EntityState(Allocation* entity) { return static_cast<uint8_t*>(Object(entity)) + kEntityState; }
// A counted reference held by this code; released on scope exit the way the game releases its own
struct EntityRef
{
Allocation* ptr = nullptr;
EntityRef() = default;
explicit EntityRef(Allocation* allocation) : ptr(allocation) { if (ptr) InterlockedIncrement(&ptr->references); }
// Takes over a reference the game has already counted (a counted pointer it returned)
static void Adopt(EntityRef& ref, Allocation* allocation) { ref.ptr = allocation; }
EntityRef(const EntityRef&) = delete;
EntityRef& operator=(const EntityRef&) = delete;
~EntityRef() { if (ptr) entity_ptr_dtor(&ptr); }
};
struct ZoneRef
{
Allocation* ptr = nullptr;
~ZoneRef() { if (ptr) zone_ptr_dtor(&ptr); }
};
struct InfoRef
{
void* ptr = nullptr;
~InfoRef() { if (ptr) remove_reference(ptr); }
};
// The entity manager's pause for an entity, set before a live edit and cleared after it, as the dev build does when it
// edits a composite template live
struct PauseContext
{
uint8_t* manager;
Allocation* entity;
PauseContext(uint8_t* m, Allocation* e) : manager(m), entity(e) { requires_push_pause_context(manager, &entity); }
~PauseContext() { requires_pop_pause_context(manager, &entity); }
};
// Held while touching the entity manager's data from outside its own calls
struct ManagerAccess
{
uint8_t storage[8] = {};
ManagerAccess() { access_lock(storage); }
~ManagerAccess() { access_unlock(storage); }
};
std::string Hex(uint32_t value)
{
char buffer[16];
snprintf(buffer, sizeof(buffer), "%02X-%02X-%02X-%02X", value & 0xFF, (value >> 8) & 0xFF, (value >> 16) & 0xFF, value >> 24);
return buffer;
}
std::vector<Allocation*> ArrayItems(void* arrayPtr)
{
std::vector<Allocation*> items;
Allocation* array = static_cast<Allocation*>(arrayPtr);
if (!array || !array->data)
return items;
Allocation** data = static_cast<Allocation**>(array->data);
for (uint32_t i = 0; i < array->count; i++)
if (data[i])
items.push_back(data[i]);
return items;
}
CompositeTemplate* FindTemplate(uint32_t guid)
{
const PackedArray& templates = *reinterpret_cast<PackedArray*>(EntityManager() + kManagerTemplates);
const uint32_t* offsets = Packed<uint32_t>(templates.offset);
if (!offsets)
return nullptr;
for (uint32_t i = 0; i < templates.count; i++)
{
CompositeTemplate* candidate = Packed<CompositeTemplate>(offsets[i]);
if (candidate && candidate->reference == guid)
return candidate;
}
return nullptr;
}
// A direct child of an instance (not through aliases)
Allocation* FindChild(Allocation* instance, uint32_t guid)
{
void* entities = *reinterpret_cast<void**>(static_cast<uint8_t*>(Object(instance)) + kInstanceEntities);
for (Allocation* child : ArrayItems(entities))
if (Object(child) && EntityGuid(child) == guid)
return child;
return nullptr;
}
// ---- Structured exception guard: a fault inside game code is reported instead of taking the game down ----
int Filter(unsigned int code, const char* what)
{
DevTools::Log("LiveLink: exception 0x%08X during %s", code, what);
return EXCEPTION_EXECUTE_HANDLER;
}
bool Guarded(const char* what, void(*body)(void*), void* context)
{
__try
{
body(context);
return true;
}
__except (Filter(GetExceptionCode(), what))
{
return false;
}
}
// ---- Variables ----
const VariableType* TypeFromGuid(uint32_t guid)
{
for (const VariableType& type : kVariableTypes)
if (*reinterpret_cast<const uint32_t*>(Address(type.guidRva)) == guid)
return &type;
return nullptr;
}
const VariableType* TypeFromVTable(uint32_t vtable)
{
for (const VariableType& type : kVariableTypes)
if (Address(type.vtableRva) == vtable)
return &type;
return nullptr;
}
std::string VariableString(const uint32_t* variable)
{
void* table = StringTable();
const char* text = table ? string_from_offset(table, variable[1]) : nullptr;
return text ? text : "";
}
// Floats by value, not by their bits: -0 and +0 are the same number, and a writer that shares equal values between
// parameters (CathodeLib's does) can hand an unchanged parameter the other one - which, compared bit for bit, made a
// new variable in the root re-initialise every instance placed in it (a mission included)
bool FloatsEqual(const uint32_t* a, const uint32_t* b, uint32_t count)
{
for (uint32_t i = 0; i < count; i++)
{
float x, y;
memcpy(&x, a + i, sizeof(float));
memcpy(&y, b + i, sizeof(float));
if (!(x == y) && a[i] != b[i]) // the bits as well, so the same NaN counts as unchanged
return false;
}
return true;
}
bool VariablesEqual(const uint32_t* a, const uint32_t* b)
{
if (!a || !b)
return a == b;
if (a[0] != b[0])
return false;
const VariableType* type = TypeFromVTable(a[0]);
switch (type ? type->kind : VariableKind::Unknown)
{
case VariableKind::String:
case VariableKind::FilePath:
return VariableString(a) == VariableString(b);
case VariableKind::Enum:
return memcmp(a + 1, b + 1, 8) == 0;
case VariableKind::Float:
return FloatsEqual(a + 1, b + 1, 1);
case VariableKind::Direction:
return FloatsEqual(a + 1, b + 1, 3);
case VariableKind::Position:
return FloatsEqual(a + 1, b + 1, 6);
case VariableKind::SplineData:
{
if (a[2] != b[2])
return false;
const uint32_t* pointsA = Packed<uint32_t>(a[1]);
const uint32_t* pointsB = Packed<uint32_t>(b[1]);
return a[2] == 0 || (pointsA && pointsB && FloatsEqual(pointsA, pointsB, a[2] * 6));
}
default:
return a[1] == b[1];
}
}
struct EntityParameter { uint32_t param; uint32_t variable; };
struct EntityParameterPack { uint32_t path; PackedArray params; };
struct EntityInitialiserData { uint32_t guid; uint32_t type; };
std::unordered_map<uint32_t, const EntityParameterPack*> ParameterPacks(const CompositeTemplate* composite)
{
std::unordered_map<uint32_t, const EntityParameterPack*> packs;
const EntityParameterPack* data = Packed<EntityParameterPack>(composite->params.offset);
for (uint32_t i = 0; data && i < composite->params.count; i++)
packs[data[i].path] = &data[i];
return packs;
}
// A proxy record (from a composite template's proxy list): its id, the path to what it stands for, and that thing's type
struct EntityProxy { uint32_t guid; PackedArray path; uint32_t guid2; uint32_t function; };
static_assert(sizeof(EntityProxy) == 0x14, "EntityProxy layout");
std::vector<uint32_t> Words(const PackedArray& array)
{
const uint32_t* data = Packed<uint32_t>(array.offset);
return data ? std::vector<uint32_t>(data, data + array.count) : std::vector<uint32_t>();
}
std::unordered_map<uint32_t, const EntityProxy*> Proxies(const CompositeTemplate* composite)
{
std::unordered_map<uint32_t, const EntityProxy*> proxies;
const EntityProxy* data = Packed<EntityProxy>(composite->proxies.offset);
for (uint32_t i = 0; data && i < composite->proxies.count; i++)
proxies[data[i].guid] = &data[i];
return proxies;
}
bool ProxiesEqual(const EntityProxy* a, const EntityProxy* b)
{
return a->guid2 == b->guid2 && a->function == b->function && Words(a->path) == Words(b->path);
}
// Each alias's id and the path of entity ids it overrides, in order
struct AliasRecord { uint32_t guid; PackedArray path; };
std::map<uint32_t, std::vector<uint32_t>> AliasPaths(const CompositeTemplate* composite)
{
std::map<uint32_t, std::vector<uint32_t>> paths;
const AliasRecord* data = Packed<AliasRecord>(composite->aliases.offset);
for (uint32_t i = 0; data && i < composite->aliases.count; i++)
paths[data[i].guid] = Words(data[i].path);
return paths;
}
// The composite's variables (a list in its composite template, which an instance walks to find them): a
// variable's default is a parameter pack under its id, read through the instance itself - not an entity of it
struct ConnectorRecord { uint32_t guid; uint32_t type; uint32_t name; };
static_assert(sizeof(ConnectorRecord) == 12, "ConnectorRecord layout");
void ConnectorIds(const CompositeTemplate* composite, std::unordered_set<uint32_t>& ids)
{
const ConnectorRecord* data = Packed<ConnectorRecord>(composite->connectors.offset);
for (uint32_t i = 0; data && i < composite->connectors.count; i++)
ids.insert(data[i].guid);
}
// Parameters that do not change what an entity does, and that OpenCAGE adds or retypes when it first opens a level it
// did not write (so a game running such a PAK would otherwise see them change on every entity at the first push, and
// every entity would be reset): 'name' (from its name tables) and 'mapping' (a string parameter turned into a material mapping)
constexpr uint32_t kIgnoredParameters[] = { 0x58EBAC79 /* name */, 0x0EE7FAC6 /* mapping */ };
std::map<uint32_t, const uint32_t*> PackVariables(const EntityParameterPack* pack)
{
std::map<uint32_t, const uint32_t*> variables;
const EntityParameter* params = pack ? Packed<EntityParameter>(pack->params.offset) : nullptr;
for (uint32_t i = 0; params && i < pack->params.count; i++)
{
bool ignored = false;
for (uint32_t id : kIgnoredParameters)
ignored |= params[i].param == id;
if (!ignored)
variables[params[i].param] = Packed<uint32_t>(params[i].variable);
}
return variables;
}
// ---- CAGEAnimation / TriggerSequence records and resource references (layouts as CathodeLib writes them to COMMANDS.PAK) ----
struct Hasher
{
uint64_t value = 14695981039346656037ull; // FNV-1a
void Word(uint32_t word)
{
for (int i = 0; i < 4; i++)
value = (value ^ ((word >> (8 * i)) & 0xFF)) * 1099511628211ull;
}
void Words(const uint32_t* words, uint32_t count)
{
for (uint32_t i = 0; i < count; i++)
Word(words[i]);
}
};
constexpr uint32_t kMaxRecordWords = 1u << 22;
// count words at a packed offset, or false (nothing there / implausibly many)
bool WordsAt(uint32_t offset, uint32_t count, const uint32_t*& words)
{
words = nullptr;
if (count == 0)
return true;
if (count > kMaxRecordWords)
return false;
words = Packed<uint32_t>(offset);
return words != nullptr;
}
// A list of track offsets, each to { min, max, id, keyframes offset, keyframe count }
void HashTracks(Hasher& hash, uint32_t listOffset, uint32_t count, uint32_t keyframeWords)
{
const uint32_t* list;
hash.Word(count);
if (!WordsAt(listOffset, count, list))
return;
for (uint32_t i = 0; i < count; i++)
{
const uint32_t* track = Packed<uint32_t>(list[i]);
const uint32_t* keys;
if (!track || !WordsAt(track[3], track[4] * keyframeWords, keys))
continue;
hash.Words(track, 3);
hash.Word(track[4]);
hash.Words(keys, track[4] * keyframeWords);
}
}
// Entity id -> content hash of its CAGEAnimation record: { id, headers, count, float tracks, count, event tracks, count }
std::unordered_map<uint32_t, uint64_t> AnimationHashes(const CompositeTemplate* composite)
{
std::unordered_map<uint32_t, uint64_t> hashes;
const uint32_t* list;
if (!WordsAt(composite->animations.offset, composite->animations.count, list))
return hashes;
for (uint32_t i = 0; i < composite->animations.count; i++)
{
const uint32_t* record = Packed<uint32_t>(list[i]);
if (!record)
continue;
Hasher hash;
const uint32_t* headers;
hash.Word(record[2]);
if (WordsAt(record[1], record[2] * 8, headers))
{
for (uint32_t h = 0; h < record[2]; h++)
{
const uint32_t* header = headers + h * 8; // binding, type, track, param, param type, sub param, path, length
const uint32_t* path;
hash.Words(header, 6);
hash.Word(header[7]);
if (WordsAt(header[6], header[7], path))
hash.Words(path, header[7]);
}
}
HashTracks(hash, record[3], record[4], 8); // float keys: mode, time, value, tangent in, tangent out
HashTracks(hash, record[5], record[6], 6); // event keys: mode, time, forward, reverse, track type, duration
hashes[record[0]] = hash.value;
}
return hashes;
}
// Entity id -> content hash of its TriggerSequence record: { id, entries, count, methods, count }
std::unordered_map<uint32_t, uint64_t> SequenceHashes(const CompositeTemplate* composite)
{
std::unordered_map<uint32_t, uint64_t> hashes;
const uint32_t* list;
if (!WordsAt(composite->sequences.offset, composite->sequences.count, list))
return hashes;
for (uint32_t i = 0; i < composite->sequences.count; i++)
{
const uint32_t* record = Packed<uint32_t>(list[i]);
if (!record)
continue;
Hasher hash;
const uint32_t* entries;
hash.Word(record[2]);
if (WordsAt(record[1], record[2] * 3, entries))
{
for (uint32_t e = 0; e < record[2]; e++)
{
const uint32_t* entry = entries + e * 3; // path, length, timing
const uint32_t* path;
hash.Word(entry[1]);
hash.Word(entry[2]);
if (WordsAt(entry[0], entry[1], path))
hash.Words(path, entry[1]);
}
}
const uint32_t* methods;
hash.Word(record[4]);
if (WordsAt(record[3], record[4] * 3, methods))
hash.Words(methods, record[4] * 3);
hashes[record[0]] = hash.value;
}
return hashes;
}
// A resource reference: 10 words with no offsets in them (position, rotation, resource id, type, two for the type)
using ResourceWords = std::array<uint32_t, 10>;
// A composite's resource references sorted and without repeats, the position and rotation as numbers (-0 is 0): their
// order and repeats say nothing, and CathodeLib gathers them entity by entity and drops repeats, so an unchanged
// composite comes back from it reordered (seen on every level). False if they can't be read.
bool ResourceSet(const PackedArray& array, std::vector<ResourceWords>& set)
{
const uint32_t* words;
if (!WordsAt(array.offset, array.count * 10, words))
return false;
set.resize(array.count);
for (uint32_t i = 0; i < array.count; i++)
{
memcpy(set[i].data(), words + i * 10, sizeof(ResourceWords));
for (int f = 0; f < 6; f++)
if (set[i][f] == 0x80000000u)
set[i][f] = 0;
}
std::sort(set.begin(), set.end());
set.erase(std::unique(set.begin(), set.end()), set.end());
return true;
}
// A link, as the template keeps it: the entity it is kept on, that entity's parameter, the entity it links to and
// that one's parameter. Records of { entity, offset, count } point at the links, each { id, parameter, linked
// parameter, linked entity } - the id is the editor's own and says nothing about where anything goes.
using LinkKey = std::array<uint32_t, 4>;
struct LinkRecord { uint32_t entity; uint32_t offset; uint32_t count; };
static_assert(sizeof(LinkRecord) == 12, "LinkRecord layout");
std::set<LinkKey> LinkSet(const CompositeTemplate* composite)
{
std::set<LinkKey> links;
const LinkRecord* records = Packed<LinkRecord>(composite->links.offset);
for (uint32_t i = 0; records && i < composite->links.count; i++)
{
const uint32_t* words;
if (!WordsAt(records[i].offset, records[i].count * 4, words) || !words)
continue;
for (uint32_t l = 0; l < records[i].count; l++)
links.insert({ records[i].entity, words[l * 4 + 1], words[l * 4 + 3], words[l * 4 + 2] });
}
return links;
}
struct RecordDiff
{
const CompositeTemplate* current = nullptr;
const CompositeTemplate* incoming = nullptr;
std::vector<uint32_t> changed; // entities whose animation or sequence record differs (or appeared/went)
bool resourcesChanged = false;
std::vector<LinkKey> linksChanged; // links there in one template and not the other
};
void RecordDiffBody(void* context)
{
RecordDiff& diff = *static_cast<RecordDiff*>(context);
for (int kind = 0; kind < 2; kind++)
{
const auto before = kind == 0 ? AnimationHashes(diff.current) : SequenceHashes(diff.current);
const auto after = kind == 0 ? AnimationHashes(diff.incoming) : SequenceHashes(diff.incoming);
for (const auto& entry : after)
{
auto match = before.find(entry.first);
if (match == before.end() || match->second != entry.second)
diff.changed.push_back(entry.first);
}
for (const auto& entry : before)
if (!after.count(entry.first))
diff.changed.push_back(entry.first);
}
std::vector<ResourceWords> resourcesA, resourcesB;
diff.resourcesChanged = !ResourceSet(diff.current->resources, resourcesA) || !ResourceSet(diff.incoming->resources, resourcesB) ||
resourcesA != resourcesB;
const std::set<LinkKey> linksA = LinkSet(diff.current);
const std::set<LinkKey> linksB = LinkSet(diff.incoming);
std::set_symmetric_difference(linksA.begin(), linksA.end(), linksB.begin(), linksB.end(), std::back_inserter(diff.linksChanged));
}
bool PacksEqual(const EntityParameterPack* a, const EntityParameterPack* b)
{
const std::map<uint32_t, const uint32_t*> variablesA = PackVariables(a);
const std::map<uint32_t, const uint32_t*> variablesB = PackVariables(b);
if (variablesA.size() != variablesB.size())
return false;
for (const auto& entry : variablesB)
{
auto match = variablesA.find(entry.first);
if (match == variablesA.end() || !VariablesEqual(match->second, entry.second))
return false;
}
return true;
}
// ---- Per-entity operations (each run inside the exception guard) ----
struct EntityOperation
{
Allocation* instance = nullptr;
uint32_t guid = 0;
uint32_t type = 0;
const void* proxy = nullptr; // for a proxy being added: its proxy record in the new template
Allocation* entity = nullptr; // for a live edit: the entity, when it was found beforehand (the caller holds a reference)
std::string outcome;
};
// Bit 28 of an entity's state flags: set once initialised, clear again after shutdown (what the entity manager tests
// before calling a method on an entity, and what the game tests when it checks that the record made to go with a call
// into an entity is still valid)
bool Initialised(Allocation* entity)
{
return ((*reinterpret_cast<const uint32_t*>(EntityState(entity)) >> 28) & 1) != 0;
}
// By its vtable: an entity's type being a composite's id is not enough - a proxy to an instance carries that type too
bool IsCompositeInstance(Allocation* entity)
{
void* object = Object(entity);
return object && *reinterpret_cast<uintptr_t*>(object) == Address(kCompositeInstanceVTable);
}
// An entity of the instance by id: a child, or what an alias of the composite points at (the instance's own lookup).
// Returned with a reference held for the caller.
Allocation* ResolveEntity(Allocation* instance, uint32_t guid)
{
Allocation* child = FindChild(instance, guid);
if (child)
{
InterlockedIncrement(&child->references);
return child;
}
void* instanceObject = Object(instance);
Allocation* found = nullptr;
Virtual<Allocation** (__thiscall*)(void*, Allocation**, const uint32_t*)>(instanceObject, kVFindEntity)(instanceObject, &found, &guid);
return found;
}
// The instances a request names: the one at the path of composite-instance entity ids from the root (checked to be an
// instance of the composite), or every running instance of the composite when the path is empty. False, with why, when
// they cannot be found.
bool FindInstances(uint32_t compositeGuid, const std::vector<uint32_t>& path, std::vector<Allocation*>& instances, std::string& error)
{
if (!path.empty())
{
Allocation* instance = *reinterpret_cast<Allocation**>(EntityManager() + kManagerRoot);
for (uint32_t step : path)
{
instance = instance ? FindChild(instance, step) : nullptr;
if (!instance || !IsCompositeInstance(instance))
{
error = "The instance path does not resolve in the running level (at " + Hex(step) + ")";
return false;
}
}
if (instance && FindTemplate(compositeGuid) && *reinterpret_cast<uint32_t*>(static_cast<uint8_t*>(Object(instance)) + kInstanceTemplate) != 0 &&
Packed<CompositeTemplate>(*reinterpret_cast<uint32_t*>(static_cast<uint8_t*>(Object(instance)) + kInstanceTemplate)) != FindTemplate(compositeGuid))
{
error = "The instance at that path is not an instance of composite " + Hex(compositeGuid);
return false;
}
instances.push_back(instance);
return true;
}
CompositeTemplate* composite = FindTemplate(compositeGuid);
if (!composite)
{
error = "Composite " + Hex(compositeGuid) + " is not in the running level";
return false;
}
instances = ArrayItems(composite->instances);
return true;
}
void FlushEntity(Allocation* entity)
{
void* object = Object(entity);
if (!object)
return;
void* entityInterface = Virtual<void* (__thiscall*)(void*)>(object, kVGetInterface)(object);
if (entityInterface)
Virtual<void(__thiscall*)(void*, Allocation**)>(entityInterface, kVFlushCache)(entityInterface, &entity);
}
// Flushes an entity's cache, reporting (rather than stopping at) an entity it faults on
bool TryFlushEntity(Allocation* entity)
{
__try
{
FlushEntity(entity);
return true;
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
return false;
}
}
void FlushOne(Allocation* entity)
{
if (!TryFlushEntity(entity))
DevTools::Log("LiveLink: could not flush the cache of %s (type %s)", Hex(EntityGuid(entity)).c_str(), Hex(EntityType(entity)).c_str());
}
void AddZones(Allocation* instance, Allocation* entity, bool add)
{
void* instanceObject = Object(instance);
auto zoneFn = add ? add_entity_to_zone : remove_entity_from_zone;
for (uint32_t slot : { kVPrimaryZone, kVSecondaryZone })
{
ZoneRef zone;
Virtual<Allocation** (__thiscall*)(void*, Allocation**, Allocation**)>(instanceObject, slot)(instanceObject, &zone.ptr, &instance);
if (zone.ptr && Object(zone.ptr))
zoneFn(Object(zone.ptr), &zone.ptr, &entity);
}
}
// An instance made from a template that has not been spawned yet has no entities: it builds them all
// from the template when it spawns, so an entity added to it now would be made twice
bool IsUnspawnedTemplate(Allocation* instance)
{
void* object = Object(instance);
if (!Virtual<bool(__thiscall*)(void*, Allocation**)>(object, kVIsTemplated)(object, &instance))
return false;
return ArrayItems(*reinterpret_cast<void**>(static_cast<uint8_t*>(object) + kInstanceEntities)).empty();
}
// An instance building its entities, for one entity or proxy: constructed and added to the instance. Initialising
// comes after every new entity is in (a later step), as an instance builds all its entities, then initialises
// them.
void ConstructBody(void* context)
{
EntityOperation& op = *static_cast<EntityOperation*>(context);
uint8_t* manager = EntityManager();
EntityRef instance(op.instance);
if (IsUnspawnedTemplate(instance.ptr))
{
op.outcome = "skipped (a template instance not spawned yet - it will be built from the new data)";
return;
}
// A retry of one that failed before may have worked in some instances already
if (FindChild(instance.ptr, op.guid))
{
op.outcome = "skipped (already in the instance)";
return;
}
EntityRef entity;
InterlockedIncrement(&instance.ptr->references); // constructing an entity or proxy takes the owner by value and releases it
if (op.proxy)
construct_proxy(manager, &entity.ptr, instance.ptr, op.proxy);
else
construct_entity(manager, &entity.ptr, instance.ptr, op.guid, op.type);
if (!entity.ptr || !Object(entity.ptr))
{
op.outcome = op.proxy ? "could not be constructed (does its target exist?)" : "could not be constructed (unknown entity type?)";
return;
}
void* object = Object(entity.ptr);
if (!op.proxy)
{
auto release = Virtual<void(__thiscall*)(void*, Allocation**)>(object, kVRelease);
// These two are dropped just as a level load drops them (an instance building its entities makes the same
// checks): not failures
if (Virtual<bool(__thiscall*)(void*, Allocation**)>(object, kVMarkedForDelete)(object, &entity.ptr))
{
release(object, &entity.ptr);
op.outcome = "skipped as on a level load (marked for delete on construction)";
return;
}
bool keep = manager[kManagerOffline] != 0;
if (!keep && !Virtual<bool(__thiscall*)(void*)>(object, kVRejected)(object))
keep = Virtual<bool(__thiscall*)(void*, Allocation**)>(object, kVRequiresScript)(object, &entity.ptr);
if (!keep)
{
release(object, &entity.ptr);
op.outcome = "skipped as on a level load (not scripted in this build of the game)";
return;
}
}
void* instanceObject = Object(instance.ptr);
Virtual<void(__thiscall*)(void*, Allocation**, Allocation**)>(instanceObject, kVAddEntity)(instanceObject, &instance.ptr, &entity.ptr);
op.entity = entity.ptr; // the reference passes to the caller, which initialises the entity next
entity.ptr = nullptr;
op.outcome = "constructed";
}
// An instance initialising its entities, for one entity just constructed and added (above)
void InitialiseAddedBody(void* context)
{
EntityOperation& op = *static_cast<EntityOperation*>(context);
uint8_t* manager = EntityManager();
EntityRef instance(op.instance);
EntityRef entity(op.entity);
PauseContext pause(manager, entity.ptr);
InfoRef info;
create_info(manager, &info.ptr, &instance.ptr, kInfoLifecycle, 0, nullptr);
state_initialise(EntityState(entity.ptr), &entity.ptr, &info.ptr);
state_validate(EntityState(entity.ptr), &entity.ptr, &info.ptr);
AddZones(instance.ptr, entity.ptr, true);
op.outcome = "added";
}
// An instance shutting down its entities, for one entity, then its removal from the instance (which releases it)
void RemoveEntityBody(void* context)
{
EntityOperation& op = *static_cast<EntityOperation*>(context);
uint8_t* manager = EntityManager();
EntityRef instance(op.instance);
EntityRef entity(FindChild(instance.ptr, op.guid));
if (!entity.ptr)
{
op.outcome = "was not in the instance";
return;
}
{
PauseContext pause(manager, entity.ptr);
InfoRef info;
create_info(manager, &info.ptr, &instance.ptr, kInfoLifecycle, 0, nullptr);
AddZones(instance.ptr, entity.ptr, false);
state_revert(EntityState(entity.ptr), &entity.ptr, &info.ptr);
state_shutdown(EntityState(entity.ptr), &entity.ptr, &info.ptr);
}
void* instanceObject = Object(instance.ptr);
Virtual<void(__thiscall*)(void*, Allocation**, Allocation**)>(instanceObject, kVRemoveEntity)(instanceObject, &instance.ptr, &entity.ptr);
op.outcome = "removed";
}
// What the dev build does to one entity when a composite template is edited live: flush its cache and live edit it. The
// id can be an alias's (an override of something inside a nested instance), which edits what it points at.
void LiveEditBody(void* context)
{
EntityOperation& op = *static_cast<EntityOperation*>(context);
uint8_t* manager = EntityManager();
EntityRef instance(op.instance);
EntityRef entity;
if (op.entity)
entity.ptr = (InterlockedIncrement(&op.entity->references), op.entity); // resolved before the template changed
else
EntityRef::Adopt(entity, ResolveEntity(instance.ptr, op.guid));
// Not (or no longer) initialised - e.g. inside an instance this same edit took out - is left alone
if (!entity.ptr || !Object(entity.ptr) || !Initialised(entity.ptr))
{
op.outcome = "was not in the instance";
return;
}
PauseContext pause(manager, entity.ptr);
// For an instance this flushes everything in it too (an instance's cache flush reaches its entities): they re-read the
// parameters that reach them through its variables
FlushEntity(entity.ptr);
InfoRef info;
create_info(manager, &info.ptr, &entity.ptr, kInfoLiveEdit, 0, nullptr);
state_live_edit(EntityState(entity.ptr), &entity.ptr, &info.ptr);
op.outcome = "live edited";
}
// Drops the cached parameters and links of the instance - its own (its variables' links) and, through the
// instance's cache flush, those of every entity in it, nested instances included - which may point at the
// replaced arrays
void FlushInstanceBody(void* context)
{
EntityOperation& op = *static_cast<EntityOperation*>(context);
FlushOne(op.instance);
op.outcome = "flushed";
}
struct MethodOperation
{
Allocation* entity = nullptr;
uint32_t method = 0;
};
void CallMethodBody(void* context)
{
MethodOperation& op = *static_cast<MethodOperation*>(context);
EntityRef entity(op.entity);
// The game's call of a method on an entity only reads the entity pointer at +0x24 of the temporary record it is given (copying
// it, with a reference, into the trigger); a zeroed record with the entity itself there stands in for it
alignas(8) uint8_t temporaryEntity[0x40] = {};
*reinterpret_cast<Allocation**>(temporaryEntity + kTemporaryEntityTarget) = entity.ptr;
call_custom_method(temporaryEntity, &entity.ptr, &op.method, 0);
// The call is queued another way than a link's, so script activity tracing (when on) notes it here: a call with no caller
LIVE_TRACE::NoteOwnCall(entity.ptr, op.method);
}
// ---- Applying an image ----
struct ApplyState
{
uint32_t compositeGuid = 0;
const uint8_t* image = nullptr;
uint32_t imageSize = 0;
const uint32_t* relocations = nullptr;
uint32_t relocationCount = 0;
CompositeTemplate* current = nullptr;
CompositeTemplate* incoming = nullptr;
std::string error;
uint32_t parameters = 0;
};
bool InBlock(const void* pointer, uint32_t size, const uint8_t* block, uint32_t blockSize)
{
const uint8_t* p = static_cast<const uint8_t*>(pointer);
return p >= block && p + size <= block + blockSize && p + size >= p;
}
// Where images live: a heap of their own, so a small image does not cost the 64 KB a direct allocation from the OS would
HANDLE ImageHeap()
{
static HANDLE heap = HeapCreate(0, 0, 0);
return heap;
}
// Rebases the image's offsets onto the pack file and checks it holds the composite; nothing outside the block is
// touched, so a block that fails here can be freed
bool RelocateAndCheck(ApplyState& state, uint8_t* block)
{
const uint32_t size = state.imageSize;
const uint32_t delta = static_cast<uint32_t>(reinterpret_cast<uintptr_t>(block) - PackFileBase());
if (delta % 4 != 0)
{
state.error = "the pack file is not word-aligned";
return false;
}
const uint32_t base = delta / 4;
uint32_t* words = reinterpret_cast<uint32_t*>(block);
const uint32_t wordCount = size / 4;
for (uint32_t i = 0; i < state.relocationCount; i++)
{
const uint32_t index = state.relocations[i];
if (index >= wordCount)
{
state.error = "a relocation lies outside the image";
return false;
}
uint32_t& word = words[index];
// A string's offset keeps its high bit ("in the pack file", as the string table reads an offset)
word = (word & 0x80000000) ? (0x80000000 | ((word + base) & 0x7FFFFFFF)) : word + base;
}
// Header: entry points (3 words), then the parameter and composite offset tables
const uint32_t parameterCount = words[4];
const uint32_t compositeCount = words[6];
const uint32_t* parameterTable = Packed<uint32_t>(words[3]);
const uint32_t* compositeTable = Packed<uint32_t>(words[5]);
if (compositeCount != 1 || !InBlock(compositeTable, 4, block, size) || parameterCount > wordCount || (parameterCount && !InBlock(parameterTable, parameterCount * 4, block, size)))
{
state.error = "the image header is not a single composite";
return false;
}
CompositeTemplate* incoming = Packed<CompositeTemplate>(compositeTable[0]);