Commit runtime loader and atlas updates

This commit is contained in:
Jan Petykiewicz 2026-04-11 18:12:25 -07:00
commit b173c50c1a
19 changed files with 8425 additions and 698 deletions

BIN
RT2.LOG Normal file

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,2 @@
query_address,function_address,name,size,calling_convention,signature,caller_count,callers,callee_count,callees,data_ref_count,data_refs,entry_excerpt
0x004ba3d0,0x004ba3d0,fcn.004ba3d0,2380,cdecl,fcn.004ba3d0();,5,0x004baedd@0x004bad20:fcn.004bad20; 0x004bb8dc@0x004baef0:fcn.004baef0; 0x004bbc89; 0x004bbd6a; 0x004bc02b,67,0x004ba53d->0x00517d40:fcn.00517d40; 0x004ba556->0x00518140:fcn.00518140; 0x004ba5b6->0x00518de0:fcn.00518de0; 0x004ba606->0x005193f0:fcn.005193f0; 0x004ba489->0x0051d820:fcn.0051d820; 0x004ba5e6->0x0051d820:fcn.0051d820; 0x004ba611->0x0051d820:fcn.0051d820; 0x004ba78a->0x0051d820:fcn.0051d820; 0x004ba830->0x0051d820:fcn.0051d820; 0x004ba8cb->0x0051d820:fcn.0051d820; 0x004ba997->0x0051d820:fcn.0051d820; 0x004bac73->0x0051d820:fcn.0051d820; 0x004ba493->0x0053b070:fcn.0053b070; 0x004ba61b->0x0053b070:fcn.0053b070; 0x004ba6d9->0x0053b070:fcn.0053b070; 0x004ba794->0x0053b070:fcn.0053b070; 0x004ba83a->0x0053b070:fcn.0053b070; 0x004ba8d5->0x0053b070:fcn.0053b070; 0x004ba9a1->0x0053b070:fcn.0053b070; 0x004baa6d->0x0053b070:fcn.0053b070; 0x004bab24->0x0053b070:fcn.0053b070; 0x004bac8f->0x0053b070:fcn.0053b070; 0x004ba5cd->0x0053c930:fcn.0053c930; 0x004ba40b->0x0053f830:fcn.0053f830; 0x004ba4c9->0x0053f830:fcn.0053f830; 0x004ba661->0x0053f830:fcn.0053f830; 0x004ba9f4->0x0053f830:fcn.0053f830; 0x004baaab->0x0053f830:fcn.0053f830; 0x004bab53->0x0053f830:fcn.0053f830; 0x004ba4b4->0x0053f9c0:fcn.0053f9c0; 0x004ba63c->0x0053f9c0:fcn.0053f9c0; 0x004ba6fd->0x0053f9c0:fcn.0053f9c0; 0x004ba7b6->0x0053f9c0:fcn.0053f9c0; 0x004ba85a->0x0053f9c0:fcn.0053f9c0; 0x004ba8f5->0x0053f9c0:fcn.0053f9c0; 0x004ba9c1->0x0053f9c0:fcn.0053f9c0; 0x004baa90->0x0053f9c0:fcn.0053f9c0; 0x004bab47->0x0053f9c0:fcn.0053f9c0; 0x004bacb2->0x0053f9c0:fcn.0053f9c0; 0x004ba421->0x0053fe00:fcn.0053fe00; 0x004ba4dc->0x0053fe00:fcn.0053fe00; 0x004ba674->0x0053fe00:fcn.0053fe00; 0x004baa0a->0x0053fe00:fcn.0053fe00; 0x004baac1->0x0053fe00:fcn.0053fe00; 0x004bab69->0x0053fe00:fcn.0053fe00; 0x004bacda->0x00540120:fcn.00540120; 0x004bacf4->0x00540120:fcn.00540120; 0x004bad0f->0x00540120:fcn.00540120; 0x004ba4a6->0x0055a040:fcn.0055a040; 0x004ba62e->0x0055a040:fcn.0055a040; 0x004ba7a9->0x0055a040:fcn.0055a040; 0x004ba84d->0x0055a040:fcn.0055a040; 0x004ba8e8->0x0055a040:fcn.0055a040; 0x004ba9b4->0x0055a040:fcn.0055a040; 0x004baca5->0x0055ab50:fcn.0055ab50; 0x004ba6ef->0x00563210:fcn.00563210; 0x004baa83->0x00563210:fcn.00563210; 0x004bab3a->0x00563210:fcn.00563210; 0x004ba4be->0x005a1145:fcn.005a1145; 0x004ba646->0x005a1145:fcn.005a1145; 0x004ba650->0x005a1145:fcn.005a1145; 0x004ba7c0->0x005a1145:fcn.005a1145; 0x004ba864->0x005a1145:fcn.005a1145; 0x004ba8ff->0x005a1145:fcn.005a1145; 0x004ba9cb->0x005a1145:fcn.005a1145; 0x004bacbf->0x005a1145:fcn.005a1145; 0x004ba56c->0x005a19c4:fcn.005a19c4,28,"0x004bacc9->0x004ba270; 0x004ba5e1->0x005d0194; 0x004ba952->0x005d0608; 0x004ba889->0x005d0614:""Caboose.imb""; 0x004ba59a->0x005d0620:""PassMail.imb""; 0x004ba593->0x005d0630:""AnyFreight.imb""; 0x004ba587->0x005d0640:""AnyCargo.imb""; 0x004ba566->0x005d0650:""%s.imb""; 0x004ba44e->0x005d0658:""Cargo.imb""; 0x004ba74e->0x005d0658:""Cargo.imb""; 0x004ba7f4->0x005d0658:""Cargo.imb""; 0x004bac25->0x005d0658:""Cargo.imb""; 0x004ba536->0x0062ba8c; 0x004ba54f->0x0062ba8c; 0x004ba3e2->0x006cfe04; 0x004ba4fa->0x006cfe04; 0x004ba546->0x006cfe04; 0x004ba576->0x006cfe04; 0x004ba711->0x006cfe04; 0x004ba7c5->0x006cfe04; 0x004ba869->0x006cfe04; 0x004ba904->0x006cfe04; 0x004ba9d0->0x006cfe04; 0x004baa95->0x006cfe04; 0x004bab81->0x006cfe04; 0x004bacdf->0x006cfe04; 0x004bacf9->0x006cfe04; 0x004ba5bb->0x006d4020"," 4ba3b0: jl 0x4ba355 <.text+0xb9355> | 4ba3b2: decl %ebx | 4ba3b3: addb %ah, 0x4ba3(%ebp) | 4ba3b9: addl %eax, (%ebx) | 4ba3bb: addl (%ebx), %eax | 4ba3bd: addl (%ebx), %eax | 4ba3bf: addb (%ebx), %al | 4ba3c1: addl (%eax), %eax | 4ba3c3: addl %eax, (%eax) | 4ba3c5: addl %edx, -0x6f6f6f70(%eax) | 4ba3cb: nop | 4ba3cc: nop | 4ba3cd: nop | 4ba3ce: nop | 4ba3cf: nop | 4ba3d0: pushl %ebp | 4ba3d1: movl %esp, %ebp | 4ba3d3: andl $-0x8, %esp | 4ba3d6: subl $0x318, %esp # imm = 0x318 | 4ba3dc: pushl %ebx | 4ba3dd: pushl %ebp | 4ba3de: pushl %esi | 4ba3df: pushl %edi | 4ba3e0: movl %ecx, %ebp | 4ba3e2: movl 0x6cfe04, %eax | 4ba3e7: cmpb $0x0, (%eax) | 4ba3ea: jbe 0x4ba72f <.text+0xb972f>"
1 query_address function_address name size calling_convention signature caller_count callers callee_count callees data_ref_count data_refs entry_excerpt
2 0x004ba3d0 0x004ba3d0 fcn.004ba3d0 2380 cdecl fcn.004ba3d0(); 5 0x004baedd@0x004bad20:fcn.004bad20; 0x004bb8dc@0x004baef0:fcn.004baef0; 0x004bbc89; 0x004bbd6a; 0x004bc02b 67 0x004ba53d->0x00517d40:fcn.00517d40; 0x004ba556->0x00518140:fcn.00518140; 0x004ba5b6->0x00518de0:fcn.00518de0; 0x004ba606->0x005193f0:fcn.005193f0; 0x004ba489->0x0051d820:fcn.0051d820; 0x004ba5e6->0x0051d820:fcn.0051d820; 0x004ba611->0x0051d820:fcn.0051d820; 0x004ba78a->0x0051d820:fcn.0051d820; 0x004ba830->0x0051d820:fcn.0051d820; 0x004ba8cb->0x0051d820:fcn.0051d820; 0x004ba997->0x0051d820:fcn.0051d820; 0x004bac73->0x0051d820:fcn.0051d820; 0x004ba493->0x0053b070:fcn.0053b070; 0x004ba61b->0x0053b070:fcn.0053b070; 0x004ba6d9->0x0053b070:fcn.0053b070; 0x004ba794->0x0053b070:fcn.0053b070; 0x004ba83a->0x0053b070:fcn.0053b070; 0x004ba8d5->0x0053b070:fcn.0053b070; 0x004ba9a1->0x0053b070:fcn.0053b070; 0x004baa6d->0x0053b070:fcn.0053b070; 0x004bab24->0x0053b070:fcn.0053b070; 0x004bac8f->0x0053b070:fcn.0053b070; 0x004ba5cd->0x0053c930:fcn.0053c930; 0x004ba40b->0x0053f830:fcn.0053f830; 0x004ba4c9->0x0053f830:fcn.0053f830; 0x004ba661->0x0053f830:fcn.0053f830; 0x004ba9f4->0x0053f830:fcn.0053f830; 0x004baaab->0x0053f830:fcn.0053f830; 0x004bab53->0x0053f830:fcn.0053f830; 0x004ba4b4->0x0053f9c0:fcn.0053f9c0; 0x004ba63c->0x0053f9c0:fcn.0053f9c0; 0x004ba6fd->0x0053f9c0:fcn.0053f9c0; 0x004ba7b6->0x0053f9c0:fcn.0053f9c0; 0x004ba85a->0x0053f9c0:fcn.0053f9c0; 0x004ba8f5->0x0053f9c0:fcn.0053f9c0; 0x004ba9c1->0x0053f9c0:fcn.0053f9c0; 0x004baa90->0x0053f9c0:fcn.0053f9c0; 0x004bab47->0x0053f9c0:fcn.0053f9c0; 0x004bacb2->0x0053f9c0:fcn.0053f9c0; 0x004ba421->0x0053fe00:fcn.0053fe00; 0x004ba4dc->0x0053fe00:fcn.0053fe00; 0x004ba674->0x0053fe00:fcn.0053fe00; 0x004baa0a->0x0053fe00:fcn.0053fe00; 0x004baac1->0x0053fe00:fcn.0053fe00; 0x004bab69->0x0053fe00:fcn.0053fe00; 0x004bacda->0x00540120:fcn.00540120; 0x004bacf4->0x00540120:fcn.00540120; 0x004bad0f->0x00540120:fcn.00540120; 0x004ba4a6->0x0055a040:fcn.0055a040; 0x004ba62e->0x0055a040:fcn.0055a040; 0x004ba7a9->0x0055a040:fcn.0055a040; 0x004ba84d->0x0055a040:fcn.0055a040; 0x004ba8e8->0x0055a040:fcn.0055a040; 0x004ba9b4->0x0055a040:fcn.0055a040; 0x004baca5->0x0055ab50:fcn.0055ab50; 0x004ba6ef->0x00563210:fcn.00563210; 0x004baa83->0x00563210:fcn.00563210; 0x004bab3a->0x00563210:fcn.00563210; 0x004ba4be->0x005a1145:fcn.005a1145; 0x004ba646->0x005a1145:fcn.005a1145; 0x004ba650->0x005a1145:fcn.005a1145; 0x004ba7c0->0x005a1145:fcn.005a1145; 0x004ba864->0x005a1145:fcn.005a1145; 0x004ba8ff->0x005a1145:fcn.005a1145; 0x004ba9cb->0x005a1145:fcn.005a1145; 0x004bacbf->0x005a1145:fcn.005a1145; 0x004ba56c->0x005a19c4:fcn.005a19c4 28 0x004bacc9->0x004ba270; 0x004ba5e1->0x005d0194; 0x004ba952->0x005d0608; 0x004ba889->0x005d0614:"Caboose.imb"; 0x004ba59a->0x005d0620:"PassMail.imb"; 0x004ba593->0x005d0630:"AnyFreight.imb"; 0x004ba587->0x005d0640:"AnyCargo.imb"; 0x004ba566->0x005d0650:"%s.imb"; 0x004ba44e->0x005d0658:"Cargo.imb"; 0x004ba74e->0x005d0658:"Cargo.imb"; 0x004ba7f4->0x005d0658:"Cargo.imb"; 0x004bac25->0x005d0658:"Cargo.imb"; 0x004ba536->0x0062ba8c; 0x004ba54f->0x0062ba8c; 0x004ba3e2->0x006cfe04; 0x004ba4fa->0x006cfe04; 0x004ba546->0x006cfe04; 0x004ba576->0x006cfe04; 0x004ba711->0x006cfe04; 0x004ba7c5->0x006cfe04; 0x004ba869->0x006cfe04; 0x004ba904->0x006cfe04; 0x004ba9d0->0x006cfe04; 0x004baa95->0x006cfe04; 0x004bab81->0x006cfe04; 0x004bacdf->0x006cfe04; 0x004bacf9->0x006cfe04; 0x004ba5bb->0x006d4020 4ba3b0: jl 0x4ba355 <.text+0xb9355> | 4ba3b2: decl %ebx | 4ba3b3: addb %ah, 0x4ba3(%ebp) | 4ba3b9: addl %eax, (%ebx) | 4ba3bb: addl (%ebx), %eax | 4ba3bd: addl (%ebx), %eax | 4ba3bf: addb (%ebx), %al | 4ba3c1: addl (%eax), %eax | 4ba3c3: addl %eax, (%eax) | 4ba3c5: addl %edx, -0x6f6f6f70(%eax) | 4ba3cb: nop | 4ba3cc: nop | 4ba3cd: nop | 4ba3ce: nop | 4ba3cf: nop | 4ba3d0: pushl %ebp | 4ba3d1: movl %esp, %ebp | 4ba3d3: andl $-0x8, %esp | 4ba3d6: subl $0x318, %esp # imm = 0x318 | 4ba3dc: pushl %ebx | 4ba3dd: pushl %ebp | 4ba3de: pushl %esi | 4ba3df: pushl %edi | 4ba3e0: movl %ecx, %ebp | 4ba3e2: movl 0x6cfe04, %eax | 4ba3e7: cmpb $0x0, (%eax) | 4ba3ea: jbe 0x4ba72f <.text+0xb972f>

View file

@ -0,0 +1,292 @@
# Analysis Context
- Target binary: `/home/jan/projects/rrt/rt3_wineprefix/drive_c/rt3/RT3.exe`
- Function names prefer the curated ledger when a committed mapping exists.
## Function Targets
### `0x004ba3d0` -> `0x004ba3d0` `fcn.004ba3d0`
- Size: `2380`
- Calling convention: `cdecl`
- Signature: `fcn.004ba3d0();`
Entry excerpt:
```asm
4ba3b0: jl 0x4ba355 <.text+0xb9355>
4ba3b2: decl %ebx
4ba3b3: addb %ah, 0x4ba3(%ebp)
4ba3b9: addl %eax, (%ebx)
4ba3bb: addl (%ebx), %eax
4ba3bd: addl (%ebx), %eax
4ba3bf: addb (%ebx), %al
4ba3c1: addl (%eax), %eax
4ba3c3: addl %eax, (%eax)
4ba3c5: addl %edx, -0x6f6f6f70(%eax)
4ba3cb: nop
4ba3cc: nop
4ba3cd: nop
4ba3ce: nop
4ba3cf: nop
4ba3d0: pushl %ebp
4ba3d1: movl %esp, %ebp
4ba3d3: andl $-0x8, %esp
4ba3d6: subl $0x318, %esp # imm = 0x318
4ba3dc: pushl %ebx
4ba3dd: pushl %ebp
4ba3de: pushl %esi
4ba3df: pushl %edi
4ba3e0: movl %ecx, %ebp
4ba3e2: movl 0x6cfe04, %eax
4ba3e7: cmpb $0x0, (%eax)
4ba3ea: jbe 0x4ba72f <.text+0xb972f>
```
Callers:
- `0x004baedd` in `0x004bad20` `fcn.004bad20`
- `0x004bb8dc` in `0x004baef0` `fcn.004baef0`
- `0x004bbc89`
- `0x004bbd6a`
- `0x004bc02b`
Caller xref excerpts:
#### `0x004baedd`
```asm
4baebd: addb %cl, 0x6cfe0415(%ebx)
4baec3: addb %cl, (%edi)
4baec5: movb $0x42, %dh
4baec7: andl %ebp, (%edx)
4baeca: pushl $0x0
4baecc: pushl %eax
4baecd: pushl $0x7d0e # imm = 0x7D0E
4baed2: pushl $0x66
4baed4: movl %esi, %ecx
4baed6: calll 0x540120 <.text+0x13f120>
4baedb: movl %esi, %ecx
4baedd: calll 0x4ba3d0 <.text+0xb93d0>
4baee2: popl %edi
4baee3: popl %esi
4baee4: popl %ebx
4baee5: retl $0x4
4baee8: nop
4baee9: nop
4baeea: nop
4baeeb: nop
4baeec: nop
4baeed: nop
4baeee: nop
4baeef: nop
4baef0: pushl %ebp
4baef1: movl %esp, %ebp
4baef3: andl $-0x8, %esp
4baef6: subl $0x270, %esp # imm = 0x270
4baefc: movl 0x6cfe04, %eax
```
#### `0x004bb8dc`
```asm
4bb8bc: incl %ebx
4bb8bd: movl %ebx, 0x2c(%esp)
4bb8c1: incl %ebp
4bb8c2: movl 0x62ba8c, %ecx
4bb8c8: incl %edi
4bb8c9: movl %edi, 0x18(%esp)
4bb8cd: calll 0x517cf0 <.text+0x116cf0>
4bb8d2: cmpl %eax, %edi
4bb8d4: jl 0x4bb490 <.text+0xba490>
4bb8da: movl %esi, %ecx
4bb8dc: calll 0x4ba3d0 <.text+0xb93d0>
4bb8e1: movl %esi, %ecx
4bb8e3: calll 0x4b9a20 <.text+0xb8a20>
4bb8e8: pushl $0x7d0b # imm = 0x7D0B
4bb8ed: movl %esi, %ecx
4bb8ef: calll 0x53f830 <.text+0x13e830>
4bb8f4: movl 0x6cec20, %ecx
4bb8fa: movl %eax, %edi
```
#### `0x004bbc89`
```asm
4bbc69: pushl $0x7d96 # imm = 0x7D96
4bbc6e: movl %ebp, %ecx
4bbc70: calll 0x53fe00 <.text+0x13ee00>
4bbc75: pushl %edi
4bbc76: pushl $0x8051 # imm = 0x8051
4bbc7b: pushl $0x8020 # imm = 0x8020
4bbc80: movl %ebp, %ecx
4bbc82: calll 0x53fe00 <.text+0x13ee00>
4bbc87: movl %ebp, %ecx
4bbc89: calll 0x4ba3d0 <.text+0xb93d0>
4bbc8e: movl 0x6cfe08, %edx
4bbc94: movb 0xc(%edx), %al
4bbc97: testb %al, %al
4bbc99: je 0x4bbca2 <.text+0xbaca2>
4bbc9b: movl %ebp, %ecx
4bbc9d: calll 0x4b9ec0 <.text+0xb8ec0>
4bbca2: popl %edi
4bbca3: popl %ebx
4bbca4: popl %esi
4bbca5: xorl %eax, %eax
4bbca7: popl %ebp
4bbca8: retl $0x4
```
#### `0x004bbd6a`
```asm
4bbd4a: movzbl (%eax), %ecx
4bbd4d: movl %edx, -0x3(%eax,%ecx,4)
4bbd51: jmp 0x4bbd68 <.text+0xbad68>
4bbd53: leal -0x3(%eax), %ecx
4bbd56: calll 0x4b99c0 <.text+0xb89c0>
4bbd5b: movl 0x6cfe04, %ecx
4bbd61: movzbl (%ecx), %edx
4bbd64: movl %eax, -0x3(%ecx,%edx,4)
4bbd68: movl %ebp, %ecx
4bbd6a: calll 0x4ba3d0 <.text+0xb93d0>
4bbd6f: movl 0x6cfe08, %eax
4bbd74: movb 0xc(%eax), %cl
4bbd77: testb %cl, %cl
4bbd79: je 0x4bbca2 <.text+0xbaca2>
4bbd7f: movl %ebp, %ecx
4bbd81: calll 0x4b9ec0 <.text+0xb8ec0>
4bbd86: popl %edi
4bbd87: popl %ebx
4bbd88: popl %esi
4bbd89: xorl %eax, %eax
```
#### `0x004bc02b`
```asm
4bc00b: addb %dl, 0x68(%edi)
4bc00e: pushl %ecx
4bc00f: addb $0x0, (%eax)
4bc012: pushl $0x8020 # imm = 0x8020
4bc017: movl %ebp, %ecx
4bc019: calll 0x53fe00 <.text+0x13ee00>
4bc01e: movl 0x6cfe04, %eax
4bc023: testb $0x40, 0x28(%eax)
4bc027: movl %ebp, %ecx
4bc029: je 0x4bc039 <.text+0xbb039>
4bc02b: calll 0x4ba3d0 <.text+0xb93d0>
4bc030: popl %edi
4bc031: popl %ebx
4bc032: popl %esi
4bc033: xorl %eax, %eax
4bc035: popl %ebp
4bc036: retl $0x4
4bc039: calll 0x4b9a20 <.text+0xb8a20>
4bc03e: popl %edi
4bc03f: popl %ebx
4bc040: popl %esi
4bc041: xorl %eax, %eax
4bc043: popl %ebp
4bc044: retl $0x4
4bc047: cmpl %edi, 0x6cfe10
```
Direct internal callees:
- `0x004ba53d` -> `0x00517d40` `fcn.00517d40`
- `0x004ba556` -> `0x00518140` `fcn.00518140`
- `0x004ba5b6` -> `0x00518de0` `fcn.00518de0`
- `0x004ba606` -> `0x005193f0` `fcn.005193f0`
- `0x004ba489` -> `0x0051d820` `fcn.0051d820`
- `0x004ba5e6` -> `0x0051d820` `fcn.0051d820`
- `0x004ba611` -> `0x0051d820` `fcn.0051d820`
- `0x004ba78a` -> `0x0051d820` `fcn.0051d820`
- `0x004ba830` -> `0x0051d820` `fcn.0051d820`
- `0x004ba8cb` -> `0x0051d820` `fcn.0051d820`
- `0x004ba997` -> `0x0051d820` `fcn.0051d820`
- `0x004bac73` -> `0x0051d820` `fcn.0051d820`
- `0x004ba493` -> `0x0053b070` `fcn.0053b070`
- `0x004ba61b` -> `0x0053b070` `fcn.0053b070`
- `0x004ba6d9` -> `0x0053b070` `fcn.0053b070`
- `0x004ba794` -> `0x0053b070` `fcn.0053b070`
- `0x004ba83a` -> `0x0053b070` `fcn.0053b070`
- `0x004ba8d5` -> `0x0053b070` `fcn.0053b070`
- `0x004ba9a1` -> `0x0053b070` `fcn.0053b070`
- `0x004baa6d` -> `0x0053b070` `fcn.0053b070`
- `0x004bab24` -> `0x0053b070` `fcn.0053b070`
- `0x004bac8f` -> `0x0053b070` `fcn.0053b070`
- `0x004ba5cd` -> `0x0053c930` `fcn.0053c930`
- `0x004ba40b` -> `0x0053f830` `fcn.0053f830`
- `0x004ba4c9` -> `0x0053f830` `fcn.0053f830`
- `0x004ba661` -> `0x0053f830` `fcn.0053f830`
- `0x004ba9f4` -> `0x0053f830` `fcn.0053f830`
- `0x004baaab` -> `0x0053f830` `fcn.0053f830`
- `0x004bab53` -> `0x0053f830` `fcn.0053f830`
- `0x004ba4b4` -> `0x0053f9c0` `fcn.0053f9c0`
- `0x004ba63c` -> `0x0053f9c0` `fcn.0053f9c0`
- `0x004ba6fd` -> `0x0053f9c0` `fcn.0053f9c0`
- `0x004ba7b6` -> `0x0053f9c0` `fcn.0053f9c0`
- `0x004ba85a` -> `0x0053f9c0` `fcn.0053f9c0`
- `0x004ba8f5` -> `0x0053f9c0` `fcn.0053f9c0`
- `0x004ba9c1` -> `0x0053f9c0` `fcn.0053f9c0`
- `0x004baa90` -> `0x0053f9c0` `fcn.0053f9c0`
- `0x004bab47` -> `0x0053f9c0` `fcn.0053f9c0`
- `0x004bacb2` -> `0x0053f9c0` `fcn.0053f9c0`
- `0x004ba421` -> `0x0053fe00` `fcn.0053fe00`
- `0x004ba4dc` -> `0x0053fe00` `fcn.0053fe00`
- `0x004ba674` -> `0x0053fe00` `fcn.0053fe00`
- `0x004baa0a` -> `0x0053fe00` `fcn.0053fe00`
- `0x004baac1` -> `0x0053fe00` `fcn.0053fe00`
- `0x004bab69` -> `0x0053fe00` `fcn.0053fe00`
- `0x004bacda` -> `0x00540120` `fcn.00540120`
- `0x004bacf4` -> `0x00540120` `fcn.00540120`
- `0x004bad0f` -> `0x00540120` `fcn.00540120`
- `0x004ba4a6` -> `0x0055a040` `fcn.0055a040`
- `0x004ba62e` -> `0x0055a040` `fcn.0055a040`
- `0x004ba7a9` -> `0x0055a040` `fcn.0055a040`
- `0x004ba84d` -> `0x0055a040` `fcn.0055a040`
- `0x004ba8e8` -> `0x0055a040` `fcn.0055a040`
- `0x004ba9b4` -> `0x0055a040` `fcn.0055a040`
- `0x004baca5` -> `0x0055ab50` `fcn.0055ab50`
- `0x004ba6ef` -> `0x00563210` `fcn.00563210`
- `0x004baa83` -> `0x00563210` `fcn.00563210`
- `0x004bab3a` -> `0x00563210` `fcn.00563210`
- `0x004ba4be` -> `0x005a1145` `fcn.005a1145`
- `0x004ba646` -> `0x005a1145` `fcn.005a1145`
- `0x004ba650` -> `0x005a1145` `fcn.005a1145`
- `0x004ba7c0` -> `0x005a1145` `fcn.005a1145`
- `0x004ba864` -> `0x005a1145` `fcn.005a1145`
- `0x004ba8ff` -> `0x005a1145` `fcn.005a1145`
- `0x004ba9cb` -> `0x005a1145` `fcn.005a1145`
- `0x004bacbf` -> `0x005a1145` `fcn.005a1145`
- `0x004ba56c` -> `0x005a19c4` `fcn.005a19c4`
Data refs:
- `0x004bacc9` -> `0x004ba270`
- `0x004ba5e1` -> `0x005d0194`
- `0x004ba952` -> `0x005d0608`
- `0x004ba889` -> `0x005d0614` "Caboose.imb"
- `0x004ba59a` -> `0x005d0620` "PassMail.imb"
- `0x004ba593` -> `0x005d0630` "AnyFreight.imb"
- `0x004ba587` -> `0x005d0640` "AnyCargo.imb"
- `0x004ba566` -> `0x005d0650` "%s.imb"
- `0x004ba44e` -> `0x005d0658` "Cargo.imb"
- `0x004ba74e` -> `0x005d0658` "Cargo.imb"
- `0x004ba7f4` -> `0x005d0658` "Cargo.imb"
- `0x004bac25` -> `0x005d0658` "Cargo.imb"
- `0x004ba536` -> `0x0062ba8c`
- `0x004ba54f` -> `0x0062ba8c`
- `0x004ba3e2` -> `0x006cfe04`
- `0x004ba4fa` -> `0x006cfe04`
- `0x004ba546` -> `0x006cfe04`
- `0x004ba576` -> `0x006cfe04`
- `0x004ba711` -> `0x006cfe04`
- `0x004ba7c5` -> `0x006cfe04`
- `0x004ba869` -> `0x006cfe04`
- `0x004ba904` -> `0x006cfe04`
- `0x004ba9d0` -> `0x006cfe04`
- `0x004baa95` -> `0x006cfe04`
- `0x004bab81` -> `0x006cfe04`
- `0x004bacdf` -> `0x006cfe04`
- `0x004bacf9` -> `0x006cfe04`
- `0x004ba5bb` -> `0x006d4020`

File diff suppressed because it is too large Load diff

View file

@ -81,8 +81,9 @@ mod tests {
use super::*; use super::*;
use crate::FixtureStateOrigin; use crate::FixtureStateOrigin;
use rrt_runtime::{ use rrt_runtime::{
CalendarPoint, RuntimeServiceState, RuntimeSnapshotDocument, RuntimeSnapshotSource, CalendarPoint, RuntimeSaveProfileState, RuntimeServiceState, RuntimeSnapshotDocument,
RuntimeState, SNAPSHOT_FORMAT_VERSION, save_runtime_snapshot_document, RuntimeSnapshotSource, RuntimeState, RuntimeWorldRestoreState, SNAPSHOT_FORMAT_VERSION,
save_runtime_snapshot_document,
}; };
use std::collections::BTreeMap; use std::collections::BTreeMap;
@ -111,8 +112,13 @@ mod tests {
tick_slot: 5, tick_slot: 5,
}, },
world_flags: BTreeMap::new(), world_flags: BTreeMap::new(),
save_profile: RuntimeSaveProfileState::default(),
world_restore: RuntimeWorldRestoreState::default(),
metadata: BTreeMap::new(),
companies: Vec::new(), companies: Vec::new(),
event_runtime_records: Vec::new(), event_runtime_records: Vec::new(),
candidate_availability: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState::default(), service_state: RuntimeServiceState::default(),
}, },
}; };

View file

@ -16,12 +16,77 @@ pub struct ExpectedRuntimeSummary {
#[serde(default)] #[serde(default)]
pub calendar: Option<rrt_runtime::CalendarPoint>, pub calendar: Option<rrt_runtime::CalendarPoint>,
#[serde(default)] #[serde(default)]
pub calendar_projection_source: Option<String>,
#[serde(default)]
pub calendar_projection_is_placeholder: Option<bool>,
#[serde(default)]
pub world_flag_count: Option<usize>, pub world_flag_count: Option<usize>,
#[serde(default)] #[serde(default)]
pub world_restore_selected_year_profile_lane: Option<u8>,
#[serde(default)]
pub world_restore_campaign_scenario_enabled: Option<bool>,
#[serde(default)]
pub world_restore_sandbox_enabled: Option<bool>,
#[serde(default)]
pub world_restore_seed_tuple_written_from_raw_lane: Option<bool>,
#[serde(default)]
pub world_restore_absolute_counter_requires_shell_context: Option<bool>,
#[serde(default)]
pub world_restore_absolute_counter_reconstructible_from_save: Option<bool>,
#[serde(default)]
pub world_restore_disable_cargo_economy_special_condition_slot: Option<u8>,
#[serde(default)]
pub world_restore_disable_cargo_economy_special_condition_reconstructible_from_save:
Option<bool>,
#[serde(default)]
pub world_restore_disable_cargo_economy_special_condition_write_side_grounded: Option<bool>,
#[serde(default)]
pub world_restore_disable_cargo_economy_special_condition_enabled: Option<bool>,
#[serde(default)]
pub world_restore_use_bio_accelerator_cars_enabled: Option<bool>,
#[serde(default)]
pub world_restore_use_wartime_cargos_enabled: Option<bool>,
#[serde(default)]
pub world_restore_disable_train_crashes_enabled: Option<bool>,
#[serde(default)]
pub world_restore_disable_train_crashes_and_breakdowns_enabled: Option<bool>,
#[serde(default)]
pub world_restore_ai_ignore_territories_at_startup_enabled: Option<bool>,
#[serde(default)]
pub world_restore_absolute_counter_restore_kind: Option<String>,
#[serde(default)]
pub world_restore_absolute_counter_adjustment_context: Option<String>,
#[serde(default)]
pub metadata_count: Option<usize>,
#[serde(default)]
pub company_count: Option<usize>, pub company_count: Option<usize>,
#[serde(default)] #[serde(default)]
pub event_runtime_record_count: Option<usize>, pub event_runtime_record_count: Option<usize>,
#[serde(default)] #[serde(default)]
pub candidate_availability_count: Option<usize>,
#[serde(default)]
pub zero_candidate_availability_count: Option<usize>,
#[serde(default)]
pub special_condition_count: Option<usize>,
#[serde(default)]
pub enabled_special_condition_count: Option<usize>,
#[serde(default)]
pub save_profile_kind: Option<String>,
#[serde(default)]
pub save_profile_family: Option<String>,
#[serde(default)]
pub save_profile_map_path: Option<String>,
#[serde(default)]
pub save_profile_display_name: Option<String>,
#[serde(default)]
pub save_profile_selected_year_profile_lane: Option<u8>,
#[serde(default)]
pub save_profile_sandbox_enabled: Option<bool>,
#[serde(default)]
pub save_profile_campaign_scenario_enabled: Option<bool>,
#[serde(default)]
pub save_profile_staged_profile_copy_on_restore: Option<bool>,
#[serde(default)]
pub total_event_record_service_count: Option<u64>, pub total_event_record_service_count: Option<u64>,
#[serde(default)] #[serde(default)]
pub periodic_boundary_call_count: Option<u64>, pub periodic_boundary_call_count: Option<u64>,
@ -45,6 +110,22 @@ impl ExpectedRuntimeSummary {
)); ));
} }
} }
if let Some(source) = &self.calendar_projection_source {
if actual.calendar_projection_source.as_ref() != Some(source) {
mismatches.push(format!(
"calendar_projection_source mismatch: expected {source:?}, got {:?}",
actual.calendar_projection_source
));
}
}
if let Some(is_placeholder) = self.calendar_projection_is_placeholder {
if actual.calendar_projection_is_placeholder != is_placeholder {
mismatches.push(format!(
"calendar_projection_is_placeholder mismatch: expected {is_placeholder}, got {}",
actual.calendar_projection_is_placeholder
));
}
}
if let Some(count) = self.world_flag_count { if let Some(count) = self.world_flag_count {
if actual.world_flag_count != count { if actual.world_flag_count != count {
mismatches.push(format!( mismatches.push(format!(
@ -53,6 +134,164 @@ impl ExpectedRuntimeSummary {
)); ));
} }
} }
if let Some(lane) = self.world_restore_selected_year_profile_lane {
if actual.world_restore_selected_year_profile_lane != Some(lane) {
mismatches.push(format!(
"world_restore_selected_year_profile_lane mismatch: expected {lane}, got {:?}",
actual.world_restore_selected_year_profile_lane
));
}
}
if let Some(enabled) = self.world_restore_campaign_scenario_enabled {
if actual.world_restore_campaign_scenario_enabled != Some(enabled) {
mismatches.push(format!(
"world_restore_campaign_scenario_enabled mismatch: expected {enabled}, got {:?}",
actual.world_restore_campaign_scenario_enabled
));
}
}
if let Some(enabled) = self.world_restore_sandbox_enabled {
if actual.world_restore_sandbox_enabled != Some(enabled) {
mismatches.push(format!(
"world_restore_sandbox_enabled mismatch: expected {enabled}, got {:?}",
actual.world_restore_sandbox_enabled
));
}
}
if let Some(enabled) = self.world_restore_seed_tuple_written_from_raw_lane {
if actual.world_restore_seed_tuple_written_from_raw_lane != Some(enabled) {
mismatches.push(format!(
"world_restore_seed_tuple_written_from_raw_lane mismatch: expected {enabled}, got {:?}",
actual.world_restore_seed_tuple_written_from_raw_lane
));
}
}
if let Some(enabled) = self.world_restore_absolute_counter_requires_shell_context {
if actual.world_restore_absolute_counter_requires_shell_context != Some(enabled) {
mismatches.push(format!(
"world_restore_absolute_counter_requires_shell_context mismatch: expected {enabled}, got {:?}",
actual.world_restore_absolute_counter_requires_shell_context
));
}
}
if let Some(enabled) = self.world_restore_absolute_counter_reconstructible_from_save {
if actual.world_restore_absolute_counter_reconstructible_from_save != Some(enabled) {
mismatches.push(format!(
"world_restore_absolute_counter_reconstructible_from_save mismatch: expected {enabled}, got {:?}",
actual.world_restore_absolute_counter_reconstructible_from_save
));
}
}
if let Some(slot) = self.world_restore_disable_cargo_economy_special_condition_slot {
if actual.world_restore_disable_cargo_economy_special_condition_slot != Some(slot) {
mismatches.push(format!(
"world_restore_disable_cargo_economy_special_condition_slot mismatch: expected {slot}, got {:?}",
actual.world_restore_disable_cargo_economy_special_condition_slot
));
}
}
if let Some(enabled) =
self.world_restore_disable_cargo_economy_special_condition_reconstructible_from_save
{
if actual
.world_restore_disable_cargo_economy_special_condition_reconstructible_from_save
!= Some(enabled)
{
mismatches.push(format!(
"world_restore_disable_cargo_economy_special_condition_reconstructible_from_save mismatch: expected {enabled}, got {:?}",
actual.world_restore_disable_cargo_economy_special_condition_reconstructible_from_save
));
}
}
if let Some(enabled) =
self.world_restore_disable_cargo_economy_special_condition_write_side_grounded
{
if actual.world_restore_disable_cargo_economy_special_condition_write_side_grounded
!= Some(enabled)
{
mismatches.push(format!(
"world_restore_disable_cargo_economy_special_condition_write_side_grounded mismatch: expected {enabled}, got {:?}",
actual.world_restore_disable_cargo_economy_special_condition_write_side_grounded
));
}
}
if let Some(enabled) = self.world_restore_disable_cargo_economy_special_condition_enabled {
if actual.world_restore_disable_cargo_economy_special_condition_enabled != Some(enabled)
{
mismatches.push(format!(
"world_restore_disable_cargo_economy_special_condition_enabled mismatch: expected {enabled}, got {:?}",
actual.world_restore_disable_cargo_economy_special_condition_enabled
));
}
}
if let Some(enabled) = self.world_restore_use_bio_accelerator_cars_enabled {
if actual.world_restore_use_bio_accelerator_cars_enabled != Some(enabled) {
mismatches.push(format!(
"world_restore_use_bio_accelerator_cars_enabled mismatch: expected {enabled}, got {:?}",
actual.world_restore_use_bio_accelerator_cars_enabled
));
}
}
if let Some(enabled) = self.world_restore_use_wartime_cargos_enabled {
if actual.world_restore_use_wartime_cargos_enabled != Some(enabled) {
mismatches.push(format!(
"world_restore_use_wartime_cargos_enabled mismatch: expected {enabled}, got {:?}",
actual.world_restore_use_wartime_cargos_enabled
));
}
}
if let Some(enabled) = self.world_restore_disable_train_crashes_enabled {
if actual.world_restore_disable_train_crashes_enabled != Some(enabled) {
mismatches.push(format!(
"world_restore_disable_train_crashes_enabled mismatch: expected {enabled}, got {:?}",
actual.world_restore_disable_train_crashes_enabled
));
}
}
if let Some(enabled) = self.world_restore_disable_train_crashes_and_breakdowns_enabled {
if actual.world_restore_disable_train_crashes_and_breakdowns_enabled != Some(enabled) {
mismatches.push(format!(
"world_restore_disable_train_crashes_and_breakdowns_enabled mismatch: expected {enabled}, got {:?}",
actual.world_restore_disable_train_crashes_and_breakdowns_enabled
));
}
}
if let Some(enabled) = self.world_restore_ai_ignore_territories_at_startup_enabled {
if actual.world_restore_ai_ignore_territories_at_startup_enabled != Some(enabled) {
mismatches.push(format!(
"world_restore_ai_ignore_territories_at_startup_enabled mismatch: expected {enabled}, got {:?}",
actual.world_restore_ai_ignore_territories_at_startup_enabled
));
}
}
if let Some(kind) = &self.world_restore_absolute_counter_restore_kind {
if actual.world_restore_absolute_counter_restore_kind.as_ref() != Some(kind) {
mismatches.push(format!(
"world_restore_absolute_counter_restore_kind mismatch: expected {kind:?}, got {:?}",
actual.world_restore_absolute_counter_restore_kind
));
}
}
if let Some(context) = &self.world_restore_absolute_counter_adjustment_context {
if actual
.world_restore_absolute_counter_adjustment_context
.as_ref()
!= Some(context)
{
mismatches.push(format!(
"world_restore_absolute_counter_adjustment_context mismatch: expected {context:?}, got {:?}",
actual.world_restore_absolute_counter_adjustment_context
));
}
}
if let Some(count) = self.metadata_count {
if actual.metadata_count != count {
mismatches.push(format!(
"metadata_count mismatch: expected {count}, got {}",
actual.metadata_count
));
}
}
if let Some(count) = self.company_count { if let Some(count) = self.company_count {
if actual.company_count != count { if actual.company_count != count {
mismatches.push(format!( mismatches.push(format!(
@ -69,6 +308,102 @@ impl ExpectedRuntimeSummary {
)); ));
} }
} }
if let Some(count) = self.candidate_availability_count {
if actual.candidate_availability_count != count {
mismatches.push(format!(
"candidate_availability_count mismatch: expected {count}, got {}",
actual.candidate_availability_count
));
}
}
if let Some(count) = self.zero_candidate_availability_count {
if actual.zero_candidate_availability_count != count {
mismatches.push(format!(
"zero_candidate_availability_count mismatch: expected {count}, got {}",
actual.zero_candidate_availability_count
));
}
}
if let Some(count) = self.special_condition_count {
if actual.special_condition_count != count {
mismatches.push(format!(
"special_condition_count mismatch: expected {count}, got {}",
actual.special_condition_count
));
}
}
if let Some(count) = self.enabled_special_condition_count {
if actual.enabled_special_condition_count != count {
mismatches.push(format!(
"enabled_special_condition_count mismatch: expected {count}, got {}",
actual.enabled_special_condition_count
));
}
}
if let Some(kind) = &self.save_profile_kind {
if actual.save_profile_kind.as_ref() != Some(kind) {
mismatches.push(format!(
"save_profile_kind mismatch: expected {kind:?}, got {:?}",
actual.save_profile_kind
));
}
}
if let Some(family) = &self.save_profile_family {
if actual.save_profile_family.as_ref() != Some(family) {
mismatches.push(format!(
"save_profile_family mismatch: expected {family:?}, got {:?}",
actual.save_profile_family
));
}
}
if let Some(map_path) = &self.save_profile_map_path {
if actual.save_profile_map_path.as_ref() != Some(map_path) {
mismatches.push(format!(
"save_profile_map_path mismatch: expected {map_path:?}, got {:?}",
actual.save_profile_map_path
));
}
}
if let Some(display_name) = &self.save_profile_display_name {
if actual.save_profile_display_name.as_ref() != Some(display_name) {
mismatches.push(format!(
"save_profile_display_name mismatch: expected {display_name:?}, got {:?}",
actual.save_profile_display_name
));
}
}
if let Some(lane) = self.save_profile_selected_year_profile_lane {
if actual.save_profile_selected_year_profile_lane != Some(lane) {
mismatches.push(format!(
"save_profile_selected_year_profile_lane mismatch: expected {lane}, got {:?}",
actual.save_profile_selected_year_profile_lane
));
}
}
if let Some(enabled) = self.save_profile_sandbox_enabled {
if actual.save_profile_sandbox_enabled != Some(enabled) {
mismatches.push(format!(
"save_profile_sandbox_enabled mismatch: expected {enabled}, got {:?}",
actual.save_profile_sandbox_enabled
));
}
}
if let Some(enabled) = self.save_profile_campaign_scenario_enabled {
if actual.save_profile_campaign_scenario_enabled != Some(enabled) {
mismatches.push(format!(
"save_profile_campaign_scenario_enabled mismatch: expected {enabled}, got {:?}",
actual.save_profile_campaign_scenario_enabled
));
}
}
if let Some(enabled) = self.save_profile_staged_profile_copy_on_restore {
if actual.save_profile_staged_profile_copy_on_restore != Some(enabled) {
mismatches.push(format!(
"save_profile_staged_profile_copy_on_restore mismatch: expected {enabled}, got {:?}",
actual.save_profile_staged_profile_copy_on_restore
));
}
}
if let Some(count) = self.total_event_record_service_count { if let Some(count) = self.total_event_record_service_count {
if actual.total_event_record_service_count != count { if actual.total_event_record_service_count != count {
mismatches.push(format!( mismatches.push(format!(

View file

@ -1,8 +1,12 @@
use std::collections::BTreeMap;
use std::path::Path; use std::path::Path;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::RuntimeState; use crate::{
CalendarPoint, RuntimeSaveProfileState, RuntimeServiceState, RuntimeState,
RuntimeWorldRestoreState, SmpLoadedSaveSlice,
};
pub const STATE_DUMP_FORMAT_VERSION: u32 = 1; pub const STATE_DUMP_FORMAT_VERSION: u32 = 1;
@ -30,6 +34,282 @@ pub struct RuntimeStateImport {
pub state: RuntimeState, pub state: RuntimeState,
} }
pub fn project_save_slice_to_runtime_state_import(
save_slice: &SmpLoadedSaveSlice,
import_id: &str,
description: Option<String>,
) -> Result<RuntimeStateImport, String> {
if import_id.trim().is_empty() {
return Err("import_id must not be empty".to_string());
}
let mut world_flags = BTreeMap::new();
world_flags.insert(
"save_slice.profile_present".to_string(),
save_slice.profile.is_some(),
);
world_flags.insert(
"save_slice.candidate_availability_present".to_string(),
save_slice.candidate_availability_table.is_some(),
);
world_flags.insert(
"save_slice.special_conditions_present".to_string(),
save_slice.special_conditions_table.is_some(),
);
world_flags.insert(
"save_slice.mechanism_confidence_grounded".to_string(),
save_slice.mechanism_confidence == "grounded",
);
if let Some(profile) = &save_slice.profile {
world_flags.insert(
"save_slice.profile_byte_0x82_nonzero".to_string(),
profile.profile_byte_0x82 != 0,
);
world_flags.insert(
"save_slice.profile_byte_0x97_nonzero".to_string(),
profile.profile_byte_0x97 != 0,
);
world_flags.insert(
"save_slice.profile_byte_0xc5_nonzero".to_string(),
profile.profile_byte_0xc5 != 0,
);
}
let mut metadata = BTreeMap::new();
metadata.insert(
"save_slice.import_projection".to_string(),
"partial-runtime-restore-v1".to_string(),
);
metadata.insert(
"save_slice.calendar_source".to_string(),
"default-1830-placeholder".to_string(),
);
metadata.insert(
"save_slice.selected_year_seed_tuple_source".to_string(),
"raw-lane-via-0x51d3f0".to_string(),
);
metadata.insert(
"save_slice.selected_year_absolute_counter_source".to_string(),
"mode-adjusted-lane-via-0x51d390-0x409e80".to_string(),
);
metadata.insert(
"save_slice.selected_year_absolute_counter_reconstructible_from_save".to_string(),
"false".to_string(),
);
metadata.insert(
"save_slice.disable_cargo_economy_special_condition_slot".to_string(),
"30".to_string(),
);
metadata.insert(
"save_slice.disable_cargo_economy_special_condition_reconstructible_from_save".to_string(),
"true".to_string(),
);
metadata.insert(
"save_slice.disable_cargo_economy_special_condition_write_side_grounded".to_string(),
"true".to_string(),
);
metadata.insert(
"save_slice.selected_year_absolute_counter_adjustment_context".to_string(),
"editor-map-mode,shell-selected-year-adjust-policy-0x9d26-0x9d28,save-special-condition-disable-cargo-economy-slot-30"
.to_string(),
);
metadata.insert(
"save_slice.mechanism_family".to_string(),
save_slice.mechanism_family.clone(),
);
metadata.insert(
"save_slice.mechanism_confidence".to_string(),
save_slice.mechanism_confidence.clone(),
);
if let Some(family) = &save_slice.container_profile_family {
metadata.insert(
"save_slice.container_profile_family".to_string(),
family.clone(),
);
}
if let Some(family) = &save_slice.trailer_family {
metadata.insert("save_slice.trailer_family".to_string(), family.clone());
}
if let Some(family) = &save_slice.bridge_family {
metadata.insert("save_slice.bridge_family".to_string(), family.clone());
}
let save_profile = if let Some(profile) = &save_slice.profile {
metadata.insert(
"save_slice.profile_kind".to_string(),
profile.profile_kind.clone(),
);
metadata.insert(
"save_slice.profile_family".to_string(),
profile.profile_family.clone(),
);
metadata.insert(
"save_slice.packed_profile_offset".to_string(),
profile.packed_profile_offset.to_string(),
);
metadata.insert(
"save_slice.packed_profile_len".to_string(),
profile.packed_profile_len.to_string(),
);
metadata.insert(
"save_slice.leading_word_0_hex".to_string(),
profile.leading_word_0_hex.clone(),
);
metadata.insert(
"save_slice.profile_byte_0x77_hex".to_string(),
profile.profile_byte_0x77_hex.clone(),
);
metadata.insert(
"save_slice.profile_byte_0x82_hex".to_string(),
profile.profile_byte_0x82_hex.clone(),
);
metadata.insert(
"save_slice.profile_byte_0x97_hex".to_string(),
profile.profile_byte_0x97_hex.clone(),
);
metadata.insert(
"save_slice.profile_byte_0xc5_hex".to_string(),
profile.profile_byte_0xc5_hex.clone(),
);
if let Some(header_flag_word_3_hex) = &profile.header_flag_word_3_hex {
metadata.insert(
"save_slice.header_flag_word_3_hex".to_string(),
header_flag_word_3_hex.clone(),
);
}
if let Some(map_path) = &profile.map_path {
metadata.insert("save_slice.map_path".to_string(), map_path.clone());
}
if let Some(display_name) = &profile.display_name {
metadata.insert("save_slice.display_name".to_string(), display_name.clone());
}
RuntimeSaveProfileState {
profile_kind: Some(profile.profile_kind.clone()),
profile_family: Some(profile.profile_family.clone()),
map_path: profile.map_path.clone(),
display_name: profile.display_name.clone(),
selected_year_profile_lane: Some(profile.profile_byte_0x77),
sandbox_enabled: Some(profile.profile_byte_0x82 != 0),
campaign_scenario_enabled: Some(profile.profile_byte_0xc5 != 0),
staged_profile_copy_on_restore: Some(profile.profile_byte_0x97 != 0),
}
} else {
RuntimeSaveProfileState::default()
};
let special_condition_enabled = |slot_index: u8| {
save_slice.special_conditions_table.as_ref().map(|table| {
table
.entries
.iter()
.find(|entry| entry.slot_index == slot_index)
.map(|entry| entry.value != 0)
.unwrap_or(false)
})
};
let world_restore = if let Some(profile) = &save_slice.profile {
let disable_cargo_economy_special_condition_enabled = special_condition_enabled(30);
RuntimeWorldRestoreState {
selected_year_profile_lane: Some(profile.profile_byte_0x77),
campaign_scenario_enabled: Some(profile.profile_byte_0xc5 != 0),
sandbox_enabled: Some(profile.profile_byte_0x82 != 0),
seed_tuple_written_from_raw_lane: Some(true),
absolute_counter_requires_shell_context: Some(true),
absolute_counter_reconstructible_from_save: Some(false),
disable_cargo_economy_special_condition_slot: Some(30),
disable_cargo_economy_special_condition_reconstructible_from_save: Some(true),
disable_cargo_economy_special_condition_write_side_grounded: Some(true),
disable_cargo_economy_special_condition_enabled,
use_bio_accelerator_cars_enabled: special_condition_enabled(29),
use_wartime_cargos_enabled: special_condition_enabled(31),
disable_train_crashes_enabled: special_condition_enabled(32),
disable_train_crashes_and_breakdowns_enabled: special_condition_enabled(33),
ai_ignore_territories_at_startup_enabled: special_condition_enabled(34),
absolute_counter_restore_kind: Some(
"mode-adjusted-selected-year-lane".to_string(),
),
absolute_counter_adjustment_context: Some(
"editor-map-mode,shell-selected-year-adjust-policy-0x9d26-0x9d28,save-special-condition-disable-cargo-economy-slot-30"
.to_string(),
),
}
} else {
RuntimeWorldRestoreState::default()
};
let mut candidate_availability = BTreeMap::new();
if let Some(table) = &save_slice.candidate_availability_table {
metadata.insert(
"save_slice.candidate_table_source_kind".to_string(),
table.source_kind.clone(),
);
metadata.insert(
"save_slice.candidate_table_semantic_family".to_string(),
table.semantic_family.clone(),
);
metadata.insert(
"save_slice.candidate_table_entry_count".to_string(),
table.observed_entry_count.to_string(),
);
metadata.insert(
"save_slice.candidate_table_zero_count".to_string(),
table.zero_availability_count.to_string(),
);
for entry in &table.entries {
candidate_availability.insert(entry.text.clone(), entry.availability_dword);
}
}
let mut special_conditions = BTreeMap::new();
if let Some(table) = &save_slice.special_conditions_table {
metadata.insert(
"save_slice.special_conditions_source_kind".to_string(),
table.source_kind.clone(),
);
metadata.insert(
"save_slice.special_conditions_table_offset".to_string(),
table.table_offset.to_string(),
);
metadata.insert(
"save_slice.special_conditions_enabled_visible_count".to_string(),
table.enabled_visible_count.to_string(),
);
for entry in &table.entries {
if !entry.hidden {
special_conditions.insert(entry.label.clone(), entry.value);
}
}
}
for (index, note) in save_slice.notes.iter().enumerate() {
metadata.insert(format!("save_slice.note.{index}"), note.clone());
}
let state = RuntimeState {
calendar: CalendarPoint {
year: 1830,
month_slot: 0,
phase_slot: 0,
tick_slot: 0,
},
world_flags,
save_profile,
world_restore,
metadata,
companies: Vec::new(),
event_runtime_records: Vec::new(),
candidate_availability,
special_conditions,
service_state: RuntimeServiceState::default(),
};
state.validate()?;
Ok(RuntimeStateImport {
import_id: import_id.to_string(),
description,
state,
})
}
pub fn validate_runtime_state_dump_document( pub fn validate_runtime_state_dump_document(
document: &RuntimeStateDumpDocument, document: &RuntimeStateDumpDocument,
) -> Result<(), String> { ) -> Result<(), String> {
@ -85,8 +365,6 @@ pub fn load_runtime_state_import_from_str(
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::{CalendarPoint, RuntimeServiceState};
use std::collections::BTreeMap;
fn state() -> RuntimeState { fn state() -> RuntimeState {
RuntimeState { RuntimeState {
@ -97,8 +375,13 @@ mod tests {
tick_slot: 0, tick_slot: 0,
}, },
world_flags: BTreeMap::new(), world_flags: BTreeMap::new(),
save_profile: RuntimeSaveProfileState::default(),
world_restore: RuntimeWorldRestoreState::default(),
metadata: BTreeMap::new(),
companies: Vec::new(), companies: Vec::new(),
event_runtime_records: Vec::new(), event_runtime_records: Vec::new(),
candidate_availability: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState::default(), service_state: RuntimeServiceState::default(),
} }
} }
@ -130,4 +413,236 @@ mod tests {
assert_eq!(import.import_id, "fallback"); assert_eq!(import.import_id, "fallback");
assert!(import.description.is_none()); assert!(import.description.is_none());
} }
#[test]
fn projects_save_slice_into_runtime_state_import() {
let save_slice = SmpLoadedSaveSlice {
file_extension_hint: Some("gms".to_string()),
container_profile_family: Some("rt3-105-save-container-v1".to_string()),
mechanism_family: "rt3-105-save-post-span-bridge-v1".to_string(),
mechanism_confidence: "mixed".to_string(),
trailer_family: Some("rt3-105-save-trailer-v1".to_string()),
bridge_family: Some("rt3-105-save-post-span-bridge-v1".to_string()),
profile: Some(crate::SmpLoadedProfile {
profile_kind: "rt3-105-packed-profile".to_string(),
profile_family: "rt3-105-save-container-v1".to_string(),
packed_profile_offset: 0x73c0,
packed_profile_len: 0x108,
packed_profile_len_hex: "0x108".to_string(),
leading_word_0: 3,
leading_word_0_hex: "0x00000003".to_string(),
header_flag_word_3: Some(0x01000000),
header_flag_word_3_hex: Some("0x01000000".to_string()),
map_path: Some("Alternate USA.gmp".to_string()),
display_name: Some("Alternate USA".to_string()),
profile_byte_0x77: 0x07,
profile_byte_0x77_hex: "0x07".to_string(),
profile_byte_0x82: 0x4d,
profile_byte_0x82_hex: "0x4d".to_string(),
profile_byte_0x97: 0x00,
profile_byte_0x97_hex: "0x00".to_string(),
profile_byte_0xc5: 0x00,
profile_byte_0xc5_hex: "0x00".to_string(),
}),
candidate_availability_table: Some(crate::SmpLoadedCandidateAvailabilityTable {
source_kind: "save-bridge-secondary-block".to_string(),
semantic_family: "scenario-named-candidate-availability-table".to_string(),
header_offset: 0x6a70,
entries_offset: 0x6ad1,
entries_end_offset: 0x73b7,
observed_entry_count: 2,
zero_availability_count: 1,
zero_availability_names: vec!["Uranium Mine".to_string()],
footer_progress_hex_words: vec!["0x000032dc".to_string(), "0x00003714".to_string()],
entries: vec![
crate::SmpRt3105SaveNameTableEntry {
index: 0,
offset: 0x6ad1,
text: "AutoPlant".to_string(),
availability_dword: 1,
availability_dword_hex: "0x00000001".to_string(),
trailer_word: 1,
trailer_word_hex: "0x00000001".to_string(),
},
crate::SmpRt3105SaveNameTableEntry {
index: 1,
offset: 0x6af3,
text: "Uranium Mine".to_string(),
availability_dword: 0,
availability_dword_hex: "0x00000000".to_string(),
trailer_word: 0,
trailer_word_hex: "0x00000000".to_string(),
},
],
}),
special_conditions_table: Some(crate::SmpLoadedSpecialConditionsTable {
source_kind: "save-fixed-special-conditions-range".to_string(),
table_offset: 0x0d64,
table_len: 36 * 4,
enabled_visible_count: 0,
enabled_visible_labels: vec![],
entries: vec![
crate::SmpSpecialConditionEntry {
slot_index: 30,
hidden: false,
label_id: 3722,
help_id: 3723,
label: "Disable Cargo Economy".to_string(),
value: 0,
value_hex: "0x00000000".to_string(),
},
crate::SmpSpecialConditionEntry {
slot_index: 35,
hidden: true,
label_id: 3,
help_id: 3,
label: "Hidden sentinel".to_string(),
value: 1,
value_hex: "0x00000001".to_string(),
},
],
}),
notes: vec!["packed profile recovered".to_string()],
};
let import = project_save_slice_to_runtime_state_import(
&save_slice,
"save-import-smoke",
Some("test save import".to_string()),
)
.expect("save slice should project");
assert_eq!(import.import_id, "save-import-smoke");
assert_eq!(
import
.state
.metadata
.get("save_slice.map_path")
.map(String::as_str),
Some("Alternate USA.gmp")
);
assert_eq!(
import.state.save_profile.selected_year_profile_lane,
Some(0x07)
);
assert_eq!(import.state.save_profile.sandbox_enabled, Some(true));
assert_eq!(
import.state.world_restore.selected_year_profile_lane,
Some(0x07)
);
assert_eq!(import.state.world_restore.sandbox_enabled, Some(true));
assert_eq!(
import.state.world_restore.campaign_scenario_enabled,
Some(false)
);
assert_eq!(
import.state.world_restore.seed_tuple_written_from_raw_lane,
Some(true)
);
assert_eq!(
import
.state
.world_restore
.absolute_counter_requires_shell_context,
Some(true)
);
assert_eq!(
import
.state
.world_restore
.absolute_counter_reconstructible_from_save,
Some(false)
);
assert_eq!(
import
.state
.world_restore
.disable_cargo_economy_special_condition_slot,
Some(30)
);
assert_eq!(
import
.state
.world_restore
.disable_cargo_economy_special_condition_reconstructible_from_save,
Some(true)
);
assert_eq!(
import
.state
.world_restore
.disable_cargo_economy_special_condition_write_side_grounded,
Some(true)
);
assert_eq!(
import
.state
.world_restore
.disable_cargo_economy_special_condition_enabled,
Some(false)
);
assert_eq!(
import.state.world_restore.use_bio_accelerator_cars_enabled,
Some(false)
);
assert_eq!(
import.state.world_restore.use_wartime_cargos_enabled,
Some(false)
);
assert_eq!(
import.state.world_restore.disable_train_crashes_enabled,
Some(false)
);
assert_eq!(
import
.state
.world_restore
.disable_train_crashes_and_breakdowns_enabled,
Some(false)
);
assert_eq!(
import
.state
.world_restore
.ai_ignore_territories_at_startup_enabled,
Some(false)
);
assert_eq!(
import
.state
.world_restore
.absolute_counter_restore_kind
.as_deref(),
Some("mode-adjusted-selected-year-lane")
);
assert_eq!(
import
.state
.world_restore
.absolute_counter_adjustment_context
.as_deref(),
Some(
"editor-map-mode,shell-selected-year-adjust-policy-0x9d26-0x9d28,save-special-condition-disable-cargo-economy-slot-30"
)
);
assert_eq!(
import.state.save_profile.map_path.as_deref(),
Some("Alternate USA.gmp")
);
assert_eq!(
import.state.candidate_availability.get("Uranium Mine"),
Some(&0)
);
assert_eq!(
import.state.special_conditions.get("Disable Cargo Economy"),
Some(&0)
);
assert_eq!(
import
.state
.world_flags
.get("save_slice.profile_byte_0x82_nonzero"),
Some(&true)
);
}
} }

View file

@ -16,7 +16,8 @@ pub use campaign_exe::{
}; };
pub use import::{ pub use import::{
RuntimeStateDumpDocument, RuntimeStateDumpSource, RuntimeStateImport, RuntimeStateDumpDocument, RuntimeStateDumpSource, RuntimeStateImport,
STATE_DUMP_FORMAT_VERSION, load_runtime_state_import, validate_runtime_state_dump_document, STATE_DUMP_FORMAT_VERSION, load_runtime_state_import,
project_save_slice_to_runtime_state_import, validate_runtime_state_dump_document,
}; };
pub use persistence::{ pub use persistence::{
RuntimeSnapshotDocument, RuntimeSnapshotSource, SNAPSHOT_FORMAT_VERSION, RuntimeSnapshotDocument, RuntimeSnapshotSource, SNAPSHOT_FORMAT_VERSION,
@ -27,16 +28,30 @@ pub use pk4::{
PK4_DIRECTORY_ENTRY_STRIDE, PK4_MAGIC, Pk4Entry, Pk4ExtractionReport, Pk4InspectionReport, PK4_DIRECTORY_ENTRY_STRIDE, PK4_MAGIC, Pk4Entry, Pk4ExtractionReport, Pk4InspectionReport,
extract_pk4_entry_bytes, extract_pk4_entry_file, inspect_pk4_bytes, inspect_pk4_file, extract_pk4_entry_bytes, extract_pk4_entry_file, inspect_pk4_bytes, inspect_pk4_file,
}; };
pub use runtime::{RuntimeCompany, RuntimeEventRecord, RuntimeServiceState, RuntimeState}; pub use runtime::{
RuntimeCompany, RuntimeEventRecord, RuntimeSaveProfileState, RuntimeServiceState, RuntimeState,
RuntimeWorldRestoreState,
};
pub use smp::{ pub use smp::{
SMP_FOUR_SIDECAR_BYTE_PLANES_MIN_BUNDLE_VERSION, SmpAsciiPreview, SmpClassicPackedProfileBlock, SMP_FOUR_SIDECAR_BYTE_PLANES_MIN_BUNDLE_VERSION, SmpAlignedRuntimeRuleBandLane,
SmpAlignedRuntimeRuleBandProbe, SmpAsciiPreview, SmpClassicPackedProfileBlock,
SmpClassicRehydrateProfileProbe, SmpContainerProfile, SmpEarlyContentProbe, SmpClassicRehydrateProfileProbe, SmpContainerProfile, SmpEarlyContentProbe,
SmpHeaderVariantProbe, SmpInspectionReport, SmpKnownTagHit, SmpPackedProfileWordLane, SmpHeaderVariantProbe, SmpInspectionReport, SmpKnownTagHit,
SmpPreamble, SmpPreambleWord, SmpRt3105PackedProfileBlock, SmpRt3105PackedProfileProbe, SmpLoadedCandidateAvailabilityTable, SmpLoadedProfile, SmpLoadedSaveSlice,
SmpRt3105PostSpanBridgeProbe, SmpRt3105SaveBridgePayloadProbe, SmpRt3105SaveNameTableEntry, SmpLoadedSpecialConditionsTable, SmpLocomotivePolicyFieldObservation,
SmpRt3105SaveNameTableProbe, SmpRuntimeAnchorCycleBlock, SmpRuntimePostSpanHeaderCandidate, SmpLocomotivePolicyFloatAlignmentCandidate, SmpLocomotivePolicyNeighborhoodProbe,
SmpRuntimePostSpanProbe, SmpRuntimeTrailerBlock, SmpSaveAnchorRunBlock, SmpSaveBootstrapBlock, SmpPackedProfileWordLane, SmpPostSpecialConditionsScalarLane,
SmpSecondaryVariantProbe, SmpSharedHeader, inspect_smp_bytes, inspect_smp_file, SmpPostSpecialConditionsScalarProbe, SmpPostTextFieldNeighborhoodProbe,
SmpPostTextFloatAlignmentCandidate, SmpPostTextGroundedFieldObservation,
SmpPreRecipeScalarPlateauLane, SmpPreRecipeScalarPlateauProbe, SmpPreamble, SmpPreambleWord,
SmpRecipeBookLineSummary, SmpRecipeBookSummaryBook, SmpRecipeBookSummaryProbe,
SmpRt3105PackedProfileBlock, SmpRt3105PackedProfileProbe, SmpRt3105PostSpanBridgeProbe,
SmpRt3105SaveBridgePayloadProbe, SmpRt3105SaveNameTableEntry, SmpRt3105SaveNameTableProbe,
SmpRuntimeAnchorCycleBlock, SmpRuntimePostSpanHeaderCandidate, SmpRuntimePostSpanProbe,
SmpRuntimeTrailerBlock, SmpSaveAnchorRunBlock, SmpSaveBootstrapBlock,
SmpSaveLoadCandidateTableSummary, SmpSaveLoadSummary, SmpSecondaryVariantProbe,
SmpSharedHeader, SmpSpecialConditionEntry, SmpSpecialConditionsProbe, inspect_smp_bytes,
inspect_smp_file, load_save_slice_file, load_save_slice_from_report,
}; };
pub use step::{BoundaryEvent, ServiceEvent, StepCommand, StepResult, execute_step_command}; pub use step::{BoundaryEvent, ServiceEvent, StepCommand, StepResult, execute_step_command};
pub use summary::RuntimeSummary; pub use summary::RuntimeSummary;

View file

@ -68,7 +68,9 @@ pub fn save_runtime_snapshot_document(
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::{CalendarPoint, RuntimeServiceState}; use crate::{
CalendarPoint, RuntimeSaveProfileState, RuntimeServiceState, RuntimeWorldRestoreState,
};
use std::collections::BTreeMap; use std::collections::BTreeMap;
fn snapshot() -> RuntimeSnapshotDocument { fn snapshot() -> RuntimeSnapshotDocument {
@ -87,8 +89,13 @@ mod tests {
tick_slot: 0, tick_slot: 0,
}, },
world_flags: BTreeMap::new(), world_flags: BTreeMap::new(),
save_profile: RuntimeSaveProfileState::default(),
world_restore: RuntimeWorldRestoreState::default(),
metadata: BTreeMap::new(),
companies: Vec::new(), companies: Vec::new(),
event_runtime_records: Vec::new(), event_runtime_records: Vec::new(),
candidate_availability: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState::default(), service_state: RuntimeServiceState::default(),
}, },
} }

View file

@ -34,16 +34,84 @@ pub struct RuntimeServiceState {
pub dirty_rerun_count: u64, pub dirty_rerun_count: u64,
} }
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct RuntimeSaveProfileState {
#[serde(default)]
pub profile_kind: Option<String>,
#[serde(default)]
pub profile_family: Option<String>,
#[serde(default)]
pub map_path: Option<String>,
#[serde(default)]
pub display_name: Option<String>,
#[serde(default)]
pub selected_year_profile_lane: Option<u8>,
#[serde(default)]
pub sandbox_enabled: Option<bool>,
#[serde(default)]
pub campaign_scenario_enabled: Option<bool>,
#[serde(default)]
pub staged_profile_copy_on_restore: Option<bool>,
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct RuntimeWorldRestoreState {
#[serde(default)]
pub selected_year_profile_lane: Option<u8>,
#[serde(default)]
pub campaign_scenario_enabled: Option<bool>,
#[serde(default)]
pub sandbox_enabled: Option<bool>,
#[serde(default)]
pub seed_tuple_written_from_raw_lane: Option<bool>,
#[serde(default)]
pub absolute_counter_requires_shell_context: Option<bool>,
#[serde(default)]
pub absolute_counter_reconstructible_from_save: Option<bool>,
#[serde(default)]
pub disable_cargo_economy_special_condition_slot: Option<u8>,
#[serde(default)]
pub disable_cargo_economy_special_condition_reconstructible_from_save: Option<bool>,
#[serde(default)]
pub disable_cargo_economy_special_condition_write_side_grounded: Option<bool>,
#[serde(default)]
pub disable_cargo_economy_special_condition_enabled: Option<bool>,
#[serde(default)]
pub use_bio_accelerator_cars_enabled: Option<bool>,
#[serde(default)]
pub use_wartime_cargos_enabled: Option<bool>,
#[serde(default)]
pub disable_train_crashes_enabled: Option<bool>,
#[serde(default)]
pub disable_train_crashes_and_breakdowns_enabled: Option<bool>,
#[serde(default)]
pub ai_ignore_territories_at_startup_enabled: Option<bool>,
#[serde(default)]
pub absolute_counter_restore_kind: Option<String>,
#[serde(default)]
pub absolute_counter_adjustment_context: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RuntimeState { pub struct RuntimeState {
pub calendar: CalendarPoint, pub calendar: CalendarPoint,
#[serde(default)] #[serde(default)]
pub world_flags: BTreeMap<String, bool>, pub world_flags: BTreeMap<String, bool>,
#[serde(default)] #[serde(default)]
pub save_profile: RuntimeSaveProfileState,
#[serde(default)]
pub world_restore: RuntimeWorldRestoreState,
#[serde(default)]
pub metadata: BTreeMap<String, String>,
#[serde(default)]
pub companies: Vec<RuntimeCompany>, pub companies: Vec<RuntimeCompany>,
#[serde(default)] #[serde(default)]
pub event_runtime_records: Vec<RuntimeEventRecord>, pub event_runtime_records: Vec<RuntimeEventRecord>,
#[serde(default)] #[serde(default)]
pub candidate_availability: BTreeMap<String, u32>,
#[serde(default)]
pub special_conditions: BTreeMap<String, u32>,
#[serde(default)]
pub service_state: RuntimeServiceState, pub service_state: RuntimeServiceState,
} }
@ -71,6 +139,79 @@ impl RuntimeState {
} }
} }
for (label, value) in [
(
"save_profile.profile_kind",
self.save_profile.profile_kind.as_deref(),
),
(
"save_profile.profile_family",
self.save_profile.profile_family.as_deref(),
),
(
"save_profile.map_path",
self.save_profile.map_path.as_deref(),
),
(
"save_profile.display_name",
self.save_profile.display_name.as_deref(),
),
] {
if value.is_some_and(|text| text.trim().is_empty()) {
return Err(format!("{label} must not be empty"));
}
}
if self.world_restore.selected_year_profile_lane.is_none()
&& (self.world_restore.campaign_scenario_enabled.is_some()
|| self.world_restore.sandbox_enabled.is_some())
{
return Err(
"world_restore.selected_year_profile_lane must be present when world restore flags are populated"
.to_string(),
);
}
if self
.world_restore
.absolute_counter_restore_kind
.as_deref()
.is_some_and(|text| text.trim().is_empty())
{
return Err(
"world_restore.absolute_counter_restore_kind must not be empty".to_string(),
);
}
if self
.world_restore
.absolute_counter_adjustment_context
.as_deref()
.is_some_and(|text| text.trim().is_empty())
{
return Err(
"world_restore.absolute_counter_adjustment_context must not be empty".to_string(),
);
}
for (key, value) in &self.metadata {
if key.trim().is_empty() {
return Err("metadata contains an empty key".to_string());
}
if value.trim().is_empty() {
return Err(format!("metadata[{key}] must not be empty"));
}
}
for key in self.candidate_availability.keys() {
if key.trim().is_empty() {
return Err("candidate_availability contains an empty key".to_string());
}
}
for key in self.special_conditions.keys() {
if key.trim().is_empty() {
return Err("special_conditions contains an empty key".to_string());
}
}
Ok(()) Ok(())
} }
} }
@ -89,6 +230,9 @@ mod tests {
tick_slot: 0, tick_slot: 0,
}, },
world_flags: BTreeMap::new(), world_flags: BTreeMap::new(),
save_profile: RuntimeSaveProfileState::default(),
world_restore: RuntimeWorldRestoreState::default(),
metadata: BTreeMap::new(),
companies: vec![ companies: vec![
RuntimeCompany { RuntimeCompany {
company_id: 1, company_id: 1,
@ -102,6 +246,53 @@ mod tests {
}, },
], ],
event_runtime_records: Vec::new(), event_runtime_records: Vec::new(),
candidate_availability: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState::default(),
};
assert!(state.validate().is_err());
}
#[test]
fn rejects_partial_world_restore_without_year_lane() {
let state = RuntimeState {
calendar: CalendarPoint {
year: 1830,
month_slot: 0,
phase_slot: 0,
tick_slot: 0,
},
world_flags: BTreeMap::new(),
save_profile: RuntimeSaveProfileState::default(),
world_restore: RuntimeWorldRestoreState {
selected_year_profile_lane: None,
campaign_scenario_enabled: Some(false),
sandbox_enabled: Some(true),
seed_tuple_written_from_raw_lane: Some(true),
absolute_counter_requires_shell_context: Some(true),
absolute_counter_reconstructible_from_save: Some(false),
disable_cargo_economy_special_condition_slot: Some(30),
disable_cargo_economy_special_condition_reconstructible_from_save: Some(true),
disable_cargo_economy_special_condition_write_side_grounded: Some(true),
disable_cargo_economy_special_condition_enabled: Some(false),
use_bio_accelerator_cars_enabled: Some(false),
use_wartime_cargos_enabled: Some(false),
disable_train_crashes_enabled: Some(false),
disable_train_crashes_and_breakdowns_enabled: Some(false),
ai_ignore_territories_at_startup_enabled: Some(false),
absolute_counter_restore_kind: Some(
"mode-adjusted-selected-year-lane".to_string(),
),
absolute_counter_adjustment_context: Some(
"editor-map-mode,shell-selected-year-adjust-policy-0x9d26-0x9d28,save-special-condition-disable-cargo-economy-slot-30".to_string(),
),
},
metadata: BTreeMap::new(),
companies: Vec::new(),
event_runtime_records: Vec::new(),
candidate_availability: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState::default(), service_state: RuntimeServiceState::default(),
}; };

File diff suppressed because it is too large Load diff

View file

@ -188,7 +188,10 @@ mod tests {
use std::collections::BTreeMap; use std::collections::BTreeMap;
use super::*; use super::*;
use crate::{CalendarPoint, RuntimeCompany, RuntimeEventRecord, RuntimeServiceState}; use crate::{
CalendarPoint, RuntimeCompany, RuntimeEventRecord, RuntimeSaveProfileState,
RuntimeServiceState, RuntimeWorldRestoreState,
};
fn state() -> RuntimeState { fn state() -> RuntimeState {
RuntimeState { RuntimeState {
@ -199,12 +202,17 @@ mod tests {
tick_slot: 0, tick_slot: 0,
}, },
world_flags: BTreeMap::new(), world_flags: BTreeMap::new(),
save_profile: RuntimeSaveProfileState::default(),
world_restore: RuntimeWorldRestoreState::default(),
metadata: BTreeMap::new(),
companies: vec![RuntimeCompany { companies: vec![RuntimeCompany {
company_id: 1, company_id: 1,
current_cash: 10, current_cash: 10,
debt: 0, debt: 0,
}], }],
event_runtime_records: Vec::new(), event_runtime_records: Vec::new(),
candidate_availability: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState::default(), service_state: RuntimeServiceState::default(),
} }
} }

View file

@ -5,9 +5,42 @@ use crate::{CalendarPoint, RuntimeState};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RuntimeSummary { pub struct RuntimeSummary {
pub calendar: CalendarPoint, pub calendar: CalendarPoint,
pub calendar_projection_source: Option<String>,
pub calendar_projection_is_placeholder: bool,
pub world_flag_count: usize, pub world_flag_count: usize,
pub world_restore_selected_year_profile_lane: Option<u8>,
pub world_restore_campaign_scenario_enabled: Option<bool>,
pub world_restore_sandbox_enabled: Option<bool>,
pub world_restore_seed_tuple_written_from_raw_lane: Option<bool>,
pub world_restore_absolute_counter_requires_shell_context: Option<bool>,
pub world_restore_absolute_counter_reconstructible_from_save: Option<bool>,
pub world_restore_disable_cargo_economy_special_condition_slot: Option<u8>,
pub world_restore_disable_cargo_economy_special_condition_reconstructible_from_save:
Option<bool>,
pub world_restore_disable_cargo_economy_special_condition_write_side_grounded: Option<bool>,
pub world_restore_disable_cargo_economy_special_condition_enabled: Option<bool>,
pub world_restore_use_bio_accelerator_cars_enabled: Option<bool>,
pub world_restore_use_wartime_cargos_enabled: Option<bool>,
pub world_restore_disable_train_crashes_enabled: Option<bool>,
pub world_restore_disable_train_crashes_and_breakdowns_enabled: Option<bool>,
pub world_restore_ai_ignore_territories_at_startup_enabled: Option<bool>,
pub world_restore_absolute_counter_restore_kind: Option<String>,
pub world_restore_absolute_counter_adjustment_context: Option<String>,
pub metadata_count: usize,
pub company_count: usize, pub company_count: usize,
pub event_runtime_record_count: usize, pub event_runtime_record_count: usize,
pub candidate_availability_count: usize,
pub zero_candidate_availability_count: usize,
pub special_condition_count: usize,
pub enabled_special_condition_count: usize,
pub save_profile_kind: Option<String>,
pub save_profile_family: Option<String>,
pub save_profile_map_path: Option<String>,
pub save_profile_display_name: Option<String>,
pub save_profile_selected_year_profile_lane: Option<u8>,
pub save_profile_sandbox_enabled: Option<bool>,
pub save_profile_campaign_scenario_enabled: Option<bool>,
pub save_profile_staged_profile_copy_on_restore: Option<bool>,
pub total_event_record_service_count: u64, pub total_event_record_service_count: u64,
pub periodic_boundary_call_count: u64, pub periodic_boundary_call_count: u64,
pub total_trigger_dispatch_count: u64, pub total_trigger_dispatch_count: u64,
@ -19,9 +52,86 @@ impl RuntimeSummary {
pub fn from_state(state: &RuntimeState) -> Self { pub fn from_state(state: &RuntimeState) -> Self {
Self { Self {
calendar: state.calendar, calendar: state.calendar,
calendar_projection_source: state.metadata.get("save_slice.calendar_source").cloned(),
calendar_projection_is_placeholder: state
.metadata
.get("save_slice.calendar_source")
.is_some_and(|value| value == "default-1830-placeholder"),
world_flag_count: state.world_flags.len(), world_flag_count: state.world_flags.len(),
world_restore_selected_year_profile_lane: state
.world_restore
.selected_year_profile_lane,
world_restore_campaign_scenario_enabled: state.world_restore.campaign_scenario_enabled,
world_restore_sandbox_enabled: state.world_restore.sandbox_enabled,
world_restore_seed_tuple_written_from_raw_lane: state
.world_restore
.seed_tuple_written_from_raw_lane,
world_restore_absolute_counter_requires_shell_context: state
.world_restore
.absolute_counter_requires_shell_context,
world_restore_absolute_counter_reconstructible_from_save: state
.world_restore
.absolute_counter_reconstructible_from_save,
world_restore_disable_cargo_economy_special_condition_slot: state
.world_restore
.disable_cargo_economy_special_condition_slot,
world_restore_disable_cargo_economy_special_condition_reconstructible_from_save: state
.world_restore
.disable_cargo_economy_special_condition_reconstructible_from_save,
world_restore_disable_cargo_economy_special_condition_write_side_grounded: state
.world_restore
.disable_cargo_economy_special_condition_write_side_grounded,
world_restore_disable_cargo_economy_special_condition_enabled: state
.world_restore
.disable_cargo_economy_special_condition_enabled,
world_restore_use_bio_accelerator_cars_enabled: state
.world_restore
.use_bio_accelerator_cars_enabled,
world_restore_use_wartime_cargos_enabled: state
.world_restore
.use_wartime_cargos_enabled,
world_restore_disable_train_crashes_enabled: state
.world_restore
.disable_train_crashes_enabled,
world_restore_disable_train_crashes_and_breakdowns_enabled: state
.world_restore
.disable_train_crashes_and_breakdowns_enabled,
world_restore_ai_ignore_territories_at_startup_enabled: state
.world_restore
.ai_ignore_territories_at_startup_enabled,
world_restore_absolute_counter_restore_kind: state
.world_restore
.absolute_counter_restore_kind
.clone(),
world_restore_absolute_counter_adjustment_context: state
.world_restore
.absolute_counter_adjustment_context
.clone(),
metadata_count: state.metadata.len(),
company_count: state.companies.len(), company_count: state.companies.len(),
event_runtime_record_count: state.event_runtime_records.len(), event_runtime_record_count: state.event_runtime_records.len(),
candidate_availability_count: state.candidate_availability.len(),
zero_candidate_availability_count: state
.candidate_availability
.values()
.filter(|value| **value == 0)
.count(),
special_condition_count: state.special_conditions.len(),
enabled_special_condition_count: state
.special_conditions
.values()
.filter(|value| **value != 0)
.count(),
save_profile_kind: state.save_profile.profile_kind.clone(),
save_profile_family: state.save_profile.profile_family.clone(),
save_profile_map_path: state.save_profile.map_path.clone(),
save_profile_display_name: state.save_profile.display_name.clone(),
save_profile_selected_year_profile_lane: state.save_profile.selected_year_profile_lane,
save_profile_sandbox_enabled: state.save_profile.sandbox_enabled,
save_profile_campaign_scenario_enabled: state.save_profile.campaign_scenario_enabled,
save_profile_staged_profile_copy_on_restore: state
.save_profile
.staged_profile_copy_on_restore,
total_event_record_service_count: state.service_state.total_event_record_services, total_event_record_service_count: state.service_state.total_event_record_services,
periodic_boundary_call_count: state.service_state.periodic_boundary_calls, periodic_boundary_call_count: state.service_state.periodic_boundary_calls,
total_trigger_dispatch_count: state total_trigger_dispatch_count: state

View file

@ -382,20 +382,24 @@ fn collect_anonymous_selector_records(
let mut records = Vec::new(); let mut records = Vec::new();
let mut start = 0usize; let mut start = 0usize;
while let Some(relative) = bytes while let Some(relative) = bytes.get(start..).and_then(|slice| {
.get(start..) slice
.and_then(|slice| slice.windows(PRELUDE.len()).position(|window| window == PRELUDE)) .windows(PRELUDE.len())
{ .position(|window| window == PRELUDE)
}) {
let record_offset = start + relative; let record_offset = start + relative;
let name_len = read_u32_le(bytes, record_offset + PRELUDE.len()).unwrap_or(0); let name_len = read_u32_le(bytes, record_offset + PRELUDE.len()).unwrap_or(0);
if name_len == 0 { if name_len == 0 {
let selector_word_0 = read_u32_le(bytes, record_offset + 0x10).unwrap_or(0); let selector_word_0 = read_u32_le(bytes, record_offset + 0x10).unwrap_or(0);
let selector_word_0_low_u16 = (selector_word_0 & 0xffff) as u16; let selector_word_0_low_u16 = (selector_word_0 & 0xffff) as u16;
if (0xc352..=0xc39b).contains(&selector_word_0_low_u16) { if (0xc352..=0xc39b).contains(&selector_word_0_low_u16) {
let preceding_named_record = let preceding_named_record = references
references.iter().rev().find(|reference| reference.offset < record_offset); .iter()
let following_named_record = .rev()
references.iter().find(|reference| reference.offset > record_offset); .find(|reference| reference.offset < record_offset);
let following_named_record = references
.iter()
.find(|reference| reference.offset > record_offset);
let selector_word_1 = read_u32_le(bytes, record_offset + 0x14).unwrap_or(0); let selector_word_1 = read_u32_le(bytes, record_offset + 0x14).unwrap_or(0);
let selector_word_0_high_u16 = ((selector_word_0 >> 16) & 0xffff) as u16; let selector_word_0_high_u16 = ((selector_word_0 >> 16) & 0xffff) as u16;
let selector_word_1_middle_u16 = ((selector_word_1 >> 8) & 0xffff) as u16; let selector_word_1_middle_u16 = ((selector_word_1 >> 8) & 0xffff) as u16;

87
rt3_auto_load_winedbg.log Normal file
View file

@ -0,0 +1,87 @@
Script started on 2026-04-08 18:28:14-07:00 [COMMAND="/opt/wine-stable/bin/winedbg --file /home/jan/projects/rrt/tools/winedbg_auto_load_compare.cmd RT3.exe" TERM="xterm-256color" TTY="/dev/pts/4" COLUMNS="116" LINES="70"]
]0;Wine Debugger[?25lWineDbg starting on pid 00f0
[?25h00ec:fixme:dbghelp:elf_search_auxv can't find symbol in module
00ec:fixme:dbghelp:elf_search_auxv can't find symbol in module
[?25lprocess_breakpoint () at /usr/src/packages/BUILD/dlls/ntdll/signal_i386.c:557
[?25h[?25l0x0000007bd237b6 process_breakpoint+0x36 [/usr/src/packages/BUILD/dlls/ntdll/signal_i386.c:557] in ntdll: movl -0x58
(%ebp), %eax
[?25h[?25lUnable to access file '/usr/src/packages/BUILD/dlls/ntdll/signal_i386.c'
[?25h[?25lBreakpoint 1 at 0x00000000438890 rt3+0x38890
[?25h[?25lBreakpoint 2 at 0x000000004390cb rt3+0x390cb
[?25h[?25lBreakpoint 3 at 0x00000000445ac0 rt3+0x45ac0
[?25h00f4:fixme:ntdll:NtQuerySystemInformation info_class SYSTEM_PERFORMANCE_INFORMATION
libEGL warning: DRI3 error: Could not get DRI3 device
libEGL warning: Ensure your X server supports DRI3 to get accelerated rendering
00f4:fixme:d3d:wined3d_guess_card No card selector available for card vendor 0000 (using GL_RENDERER "llvmpipe (LLVM 19.1.7, 256 bits)").
00f4:fixme:d3d:wined3d_guess_card No card selector available for card vendor 0000 (using GL_RENDERER "llvmpipe (LLVM 19.1.7, 256 bits)").
[?25lThread ID=0160 not in our list of threads -> can't rename
[?25h[?25lThread ID=0184 not in our list of threads -> can't rename
[?25h0140:fixme:d3d:state_linepattern_w Setting line patterns is not supported in OpenGL core contexts.
X Error of failed request: XF86VidModeClientNotLocal
Major opcode of failed request: 150 (XFree86-VidModeExtension)
Minor opcode of failed request: 15 (XF86VidModeSetGamma)
Serial number of failed request: 3081
Current serial number in output stream: 3098
[?25lInvalid address (0x00000000438890 rt3+0x38890) for breakpoint 1, disabling it
[?25h[?25lInvalid address (0x000000004390cb rt3+0x390cb) for breakpoint 2, disabling it
[?25h[?25lInvalid address (0x00000000445ac0 rt3+0x45ac0) for breakpoint 3, disabling it
[?25h[?25lProcess of pid=00f0 has terminated
[?25h
[?25lException c0000005
[?25h[?25lNo process loaded, cannot execute 'print/x *(unsigned int*)($esp)'
[?25h[?25lNo process loaded, cannot execute 'print/x *(unsigned int*)($esp+4)'
[?25h[?25lNo process loaded, cannot execute 'print/x *(unsigned int*)($esp+8)'
[?25h[?25lNo process loaded, cannot execute 'print/x *(unsigned int*)($esp+12)'
[?25h[?25lNo process loaded, cannot execute 'print/x *(unsigned int*)0x006cec74'
[?25h[?25lNo process loaded, cannot execute 'print/x *(unsigned int*)0x006cec7c'
[?25h[?25lNo process loaded, cannot execute 'print/x *(unsigned int*)0x006cec78'
[?25h[?25lNo process loaded, cannot execute 'print/x *(unsigned int*)0x006ce9b8'
[?25h[?25lNo process loaded, cannot execute 'print/x *(unsigned int*)0x006ce9bc'
[?25h[?25lNo process loaded, cannot execute 'print/x *(unsigned int*)0x006ce9c0'
[?25h[?25lNo process loaded, cannot execute 'print/x *(unsigned int*)0x006ce9c4'
[?25h[?25lNo process loaded, cannot execute 'print/x *(unsigned int*)0x006d1270'
[?25h[?25lNo process loaded, cannot execute 'print/x *(unsigned int*)0x006d1274'
[?25h[?25lNo process loaded, cannot execute 'print/x *(unsigned int*)0x006d1278'
[?25h[?25lNo process loaded, cannot execute 'print/x *(unsigned int*)0x006d127c'
[?25h[?25lYou must be attached to a process to run this command.
[?25h[?25lNo process loaded, cannot execute 'cont'
[?25h[?25lNo process loaded, cannot execute 'cont'
[?25h
[?25lException c0000005
[?25h[?25lNo process loaded, cannot execute 'print/x *(unsigned int*)($esp)'
[?25h[?25lNo process loaded, cannot execute 'print/x *(unsigned int*)($esp+4)'
[?25h[?25lNo process loaded, cannot execute 'print/x *(unsigned int*)($esp+8)'
[?25h[?25lNo process loaded, cannot execute 'print/x *(unsigned int*)($esp+12)'
[?25h[?25lNo process loaded, cannot execute 'print/x *(unsigned int*)0x006cec74'
[?25h[?25lNo process loaded, cannot execute 'print/x *(unsigned int*)0x006cec7c'
[?25h[?25lNo process loaded, cannot execute 'print/x *(unsigned int*)0x006cec78'
[?25h[?25lNo process loaded, cannot execute 'print/x *(unsigned int*)0x006ce9b8'
[?25h[?25lNo process loaded, cannot execute 'print/x *(unsigned int*)0x006ce9bc'
[?25h[?25lNo process loaded, cannot execute 'print/x *(unsigned int*)0x006ce9c0'
[?25h[?25lNo process loaded, cannot execute 'print/x *(unsigned int*)0x006ce9c4'
[?25h[?25lNo process loaded, cannot execute 'print/x *(unsigned int*)0x006d1270'
[?25h[?25lNo process loaded, cannot execute 'print/x *(unsigned int*)0x006d1274'
[?25h[?25lNo process loaded, cannot execute 'print/x *(unsigned int*)0x006d1278'
[?25h[?25lNo process loaded, cannot execute 'print/x *(unsigned int*)0x006d127c'
[?25h[?25lYou must be attached to a process to run this command.
[?25h[?25lNo process loaded, cannot execute 'cont'
[?25h
[?25lException c0000005
[?25h[?25lNo process loaded, cannot execute 'print/x *(unsigned int*)($esp)'
[?25h[?25lNo process loaded, cannot execute 'print/x *(unsigned int*)($esp+4)'
[?25h[?25lNo process loaded, cannot execute 'print/x *(unsigned int*)($esp+8)'
[?25h[?25lNo process loaded, cannot execute 'print/x *(unsigned int*)($esp+12)'
[?25h[?25lNo process loaded, cannot execute 'print/x *(unsigned int*)0x006cec74'
[?25h[?25lNo process loaded, cannot execute 'print/x *(unsigned int*)0x006cec7c'
[?25h[?25lNo process loaded, cannot execute 'print/x *(unsigned int*)0x006cec78'
[?25h[?25lNo process loaded, cannot execute 'print/x *(unsigned int*)0x006ce9b8'
[?25h[?25lNo process loaded, cannot execute 'print/x *(unsigned int*)0x006ce9bc'
[?25h[?25lNo process loaded, cannot execute 'print/x *(unsigned int*)0x006ce9c0'
[?25h[?25lNo process loaded, cannot execute 'print/x *(unsigned int*)0x006ce9c4'
[?25h[?25lNo process loaded, cannot execute 'print/x *(unsigned int*)0x006d1270'
[?25h[?25lNo process loaded, cannot execute 'print/x *(unsigned int*)0x006d1274'
[?25h[?25lNo process loaded, cannot execute 'print/x *(unsigned int*)0x006d1278'
[?25h[?25lNo process loaded, cannot execute 'print/x *(unsigned int*)0x006d127c'
[?25h[?25lYou must be attached to a process to run this command.
[?25h
Script done on 2026-04-08 18:33:15-07:00 [COMMAND_EXIT_CODE="0"]

View file

@ -0,0 +1,44 @@
Script started on 2026-04-07 18:35:01-07:00 [COMMAND="/opt/wine-stable/bin/winedbg --file /home/jan/projects/rrt/tools/winedbg_manual_load_445ac0.cmd RT3.exe" TERM="xterm-256color" TTY="/dev/pts/4" COLUMNS="116" LINES="30"]
]0;Wine Debugger[?25lWineDbg starting on pid 00f0
[?25h00ec:fixme:dbghelp:elf_search_auxv can't find symbol in module
00ec:fixme:dbghelp:elf_search_auxv can't find symbol in module
[?25lprocess_breakpoint () at /usr/src/packages/BUILD/dlls/ntdll/signal_i386.c:557
[?25h[?25l0x0000007bd237b6 process_breakpoint+0x36 [/usr/src/packages/BUILD/dlls/ntdll/signal_i386.c:557] in ntdll: movl -0x58
(%ebp), %eax
[?25h[?25lUnable to access file '/usr/src/packages/BUILD/dlls/ntdll/signal_i386.c'
[?25h[?25lBreakpoint 1 at 0x000000004390cb rt3+0x390cb
[?25h[?25lBreakpoint 2 at 0x00000000445ac0 rt3+0x45ac0
[?25h00f4:fixme:ntdll:NtQuerySystemInformation info_class SYSTEM_PERFORMANCE_INFORMATION
libEGL warning: DRI3 error: Could not get DRI3 device
libEGL warning: Ensure your X server supports DRI3 to get accelerated rendering
00f4:fixme:d3d:wined3d_guess_card No card selector available for card vendor 0000 (using GL_RENDERER "llvmpipe (LLVM 19.1.7, 256 bits)").
00f4:fixme:d3d:wined3d_guess_card No card selector available for card vendor 0000 (using GL_RENDERER "llvmpipe (LLVM 19.1.7, 256 bits)").
[?25lThread ID=0164 not in our list of threads -> can't rename
[?25h[?25lThread ID=0188 not in our list of threads -> can't rename
[?25h0144:fixme:d3d:state_linepattern_w Setting line patterns is not supported in OpenGL core contexts.
00ec:fixme:dbghelp:elf_search_auxv can't find symbol in module
[?25lStopped on breakpoint 1 at 0x000000004390cb rt3+0x390cb
[?25h[?25lRegister dump:
[?25h[?25l CS:0023 SS:002b DS:002b ES:002b FS:0000 GS:002b
EIP:004390cb ESP:0022fb30 EBP:02af5840 EFLAGS:00000206( - -- I - -P- )
[?25h[?25l EAX:00000002 EBX:00000000 ECX:02af5840 EDX:01db4739
[?25h[?25l ESI:00000000 EDI:00000001
[?25h[?25l0x1db4739
[?25h[?25l0x4
[?25h[?25l0x22fb50
[?25h[?25l0x26d7b88
[?25h[?25l0x1d81230
[?25h[?25l0x2af5840
[?25h[?25l0
[?25h[?25l0
[?25h[?25l0
[?25h[?25l0
[?25h[?25l0
[?25h[?25l0
[?25h[?25l0
[?25h[?25l0
[?25h[?25lBacktrace:
[?25h[?25l=>0 0x000000004390cb in rt3 (+0x390cb) (0x00000002af5840)
[?25h[?25l 1 0x00000001072558 (0000000000000000)
[?25h
Script done on 2026-04-07 18:35:28-07:00 [COMMAND_EXIT_CODE="0"]