Thimbleweed Park savegames
-
- Posts: 4
- Joined: Sat Apr 11, 2020 7:48 am
Thimbleweed Park savegames
Is there someone can help me do reverse engineering on the savegames of Thimbleweed Park?
Here is some information about it: https://blog.thimbleweedpark.com/savegame
I'm the author of engge https://github.com/scemino/engge, this is an open source game engine which is able run Thimbleweed Park, an awesome adventure game by Ron Gilbert.
Now the big limitations are the savegames.
It would be very nice to be able to load and save the original savegames. With your help, maybe it's possible.
Thank you
Here is some information about it: https://blog.thimbleweedpark.com/savegame
I'm the author of engge https://github.com/scemino/engge, this is an open source game engine which is able run Thimbleweed Park, an awesome adventure game by Ron Gilbert.
Now the big limitations are the savegames.
It would be very nice to be able to load and save the original savegames. With your help, maybe it's possible.
Thank you
-
- Posts: 4
- Joined: Sat Apr 11, 2020 7:48 am
Re: Thimbleweed Park savegames
Good news: I have made some progress, I bypassed the function which encrypts the data and I get the same structure as in a ggpack file (the one starting with the signature 0x04030201
Here is an example of 1 savegame converted to json, I had to remove some parts due to the size of the content
Here is an example of 1 savegame converted to json, I had to remove some parts due to the size of the content
Code: Select all
{
"actors": {
"bankmanager": {
"_costume": "BankMgrAnimation",
"_dir": 2,
"_lockFacing": 0,
"_pos": "{541,61}",
"_roomKey": "Bank",
"defaultVerb": 3,
"detective": 0,
"dialog": null,
"enterWalk": 10,
"flags": 3158024,
"gender": 3145728,
"last_selected": 0,
"name": "@30103",
"rambleTID": 0,
"sawRaysBadge": 0,
"sawReyesBadge": 0,
"selectable": 0
},
// other actors,
},
"callbacks": {
"callbacks": [],
"nextGuid": 8000000
},
"currentRoom": "Bridge",
"dialog": {},
"easy_mode": 1,
"gameGUID": "",
"gameScene":
"actorsSelectable": 0,
"actorsTempUnselectable": 0,
"forceTalkieText": 0,
"selectableActors": [
{
"_actorKey": "ray",
"selectable": 0
},
// etc.
]
},
"gameTime": 1003.47,
"globals": {
{
"abducted_agent": null,
"abducted_agent_seen": 0,
"act1": 1,
"act2": 0,
"act2_delores_intro": 0,
"act2_franklin_intro": 0,
"act2_ransome_intro": 0,
"act3": 0,
"act4": 0,
"act4_ransome_done": 0,
"act4_ray_done": 0,
"act4_reyes_done": 0,
"activeRatHole": {
"_objectKey": "bigtopHole4"
},
"actorGreetingTID": 10000375,
"agent_kidnapped": 0,
"agent_needs_dime": 0,
// etc.
},
"inputState": 101,
"inventory": {
"slots":
[
{
"objects": [
"raysBadge",
"raysNotebook",
"cellPhone"
],
"scroll": 0
},
// etc.
]
},
"objects":
"aStreetArcadeDoorWF": {
"flags": 1073742912,
"name": "@29000"
},
// etc.
},
"rooms": {
"AStreet": {
"background": "AStreet",
"speck_of_dust": 1,
"speck_of_dust_collected": 0
},
// etc.
},
"savebuild": 958,
"savetime": 1586606075,
"selectedActor": "boris",
"version": 2
}
-
- Posts: 250
- Joined: Sat Dec 27, 2014 8:49 pm
Re: Thimbleweed Park savegames
We would need some saved game files and generally the game exe / dll's (if any are used) to be able to assist.
-
- Posts: 4
- Joined: Sat Apr 11, 2020 7:48 am
Re: Thimbleweed Park savegames
atom0s wrote:We would need some saved game files and generally the game exe / dll's (if any are used) to be able to assist.
You're totally right you can find some savegames here http://www.mediafire.com/file/pck64rym4 ... rk.7z/file
The game is in Gog or steam https://www.gog.com/game/thimbleweed_pa ... gJr7fD_BwE and https://store.steampowered.com/app/5698 ... weed_Park/
-
- Posts: 250
- Joined: Sat Dec 27, 2014 8:49 pm
Re: Thimbleweed Park savegames
The game is using TEA encryption for the saved files with some extra checking afterward.
Decryption is done via:
The TEA key is:
This will decrypt the files back to a GGData object.
Decryption is done via:
Code: Select all
void __cdecl sub_4D9710(_DWORD *a1, signed int a2, int a3)
{
int v3; // ecx
unsigned int *v4; // edx
unsigned int v5; // eax
int v6; // esi
unsigned int v7; // edi
unsigned int v8; // ebx
unsigned int v9; // edx
int v10; // esi
int v11; // eax
unsigned int v12; // edx
int v13; // esi
int v14; // eax
bool v15; // zf
_DWORD *v16; // edx
int v17; // edi
int v18; // ebx
unsigned int v19; // eax
unsigned int v20; // ecx
int v21; // ebx
int v22; // esi
int v23; // edi
unsigned int *v24; // [esp+Ch] [ebp-10h]
_DWORD *v25; // [esp+Ch] [ebp-10h]
unsigned int v26; // [esp+10h] [ebp-Ch]
int i; // [esp+10h] [ebp-Ch]
int v28; // [esp+14h] [ebp-8h]
int v29; // [esp+14h] [ebp-8h]
int v30; // [esp+18h] [ebp-4h]
unsigned int v31; // [esp+28h] [ebp+Ch]
int v32; // [esp+28h] [ebp+Ch]
if ( a2 <= 1 )
{
if ( a2 < -1 )
{
v16 = a1;
v17 = -a2 - 1;
v29 = -a2 - 1;
v18 = 0x9E3779B9 * (52 / -a2 + 6);
v19 = *a1;
v25 = &a1[-a2 - 1];
v32 = 0x9E3779B9 * (52 / -a2 + 6);
do
{
v20 = v18;
v21 = v17;
for ( i = (v20 >> 2) & 3; v21; --v21 )
{
v22 = v16[v21 - 1];
v23 = (16 * v22 ^ (v19 >> 3)) + ((v16[v21 - 1] >> 5) ^ 4 * v19);
v16 = a1;
v16[v21] -= ((v32 ^ v19) + (v22 ^ *(_DWORD *)(a3 + 4 * (i ^ v21 & 3)))) ^ v23;
v19 = a1[v21];
}
v16 = a1;
*v16 -= ((v32 ^ v19) + (*v25 ^ *(_DWORD *)(a3 + 4 * (i ^ v21 & 3)))) ^ ((16 * *v25 ^ (v19 >> 3))
+ ((*v25 >> 5) ^ 4 * v19));
v15 = v32 == 0x9E3779B9;
v18 = v32 + 0x61C88647;
v19 = *a1;
v17 = v29;
v32 += 0x61C88647;
}
while ( !v15 );
}
}
else
{
v3 = 0;
v4 = a1;
v28 = 52 / a2 + 6;
v5 = a1[a2 - 1];
v6 = a2 - 1;
v24 = &a1[a2 - 1];
v31 = a1[a2 - 1];
v26 = v6;
do
{
v7 = 0;
v30 = v3 - 0x61C88647;
v8 = ((unsigned int)(v3 - 0x61C88647) >> 2) & 3;
if ( v6 )
{
do
{
v9 = v4[v7 + 1];
v10 = (16 * v31 ^ (v9 >> 3)) + ((v5 >> 5) ^ 4 * v9);
v11 = (v30 ^ v9) + (v31 ^ *(_DWORD *)(a3 + 4 * (v8 ^ v7 & 3)));
v4 = a1;
v4[v7] += v11 ^ v10;
v5 = a1[v7++];
v31 = v5;
}
while ( v7 < v26 );
}
v12 = *v4;
v13 = (16 * v31 ^ (v12 >> 3)) + ((v5 >> 5) ^ 4 * v12);
v3 -= 0x61C88647;
v14 = (v30 ^ v12) + (v31 ^ *(_DWORD *)(a3 + 4 * (v8 ^ v7 & 3)));
v4 = a1;
*v24 += v14 ^ v13;
v15 = v28-- == 1;
v5 = *v24;
v6 = v26;
v31 = *v24;
}
while ( !v15 );
}
}
char __cdecl sub_4D95E0(void *Src, size_t Size, int a3, int a4, int a5)
{
char result; // al
_DWORD *v6; // eax
_DWORD *v7; // esi
unsigned int v8; // eax
signed int v9; // edi
int v10; // edx
int v11; // ebx
int v12; // ecx
int v13; // edi
int v14; // eax
char *Srca; // [esp+10h] [ebp+8h]
size_t Sizea; // [esp+14h] [ebp+Ch]
if ( !Src || !a5 )
return 0;
if ( (signed int)Size % 8 )
return 0;
v6 = malloc(Size);
v7 = v6;
if ( v6 )
memcpy(v6, Src, Size);
sub_4D9710(v7, (signed int)Size / -4, a5);
v8 = *((unsigned __int8 *)v7 + Size - 1);
v9 = Size - v8 - 9;
Sizea = Size - v8 - 9;
if ( v8 > 8 || v9 <= 0 )
goto LABEL_23;
v10 = 0;
Srca = (char *)0x6583463;
v11 = 0;
v12 = 0;
if ( v9 >= 2 )
{
v13 = v9 - 1;
do
{
v10 += *((unsigned __int8 *)v7 + v12);
v14 = *((unsigned __int8 *)v7 + v12 + 1);
v12 += 2;
v11 += v14;
}
while ( v12 < v13 );
v9 = Sizea;
}
if ( v12 < v9 )
Srca = (char *)(*((unsigned __int8 *)v7 + v12) + 106443875);
if ( &Srca[v11 + v10] != (char *)(*((unsigned __int8 *)v7 + v9) | ((*((unsigned __int8 *)v7 + v9 + 1) | (*(unsigned __int16 *)((char *)v7 + v9 + 2) << 8)) << 8)) )
{
LABEL_23:
if ( v7 )
free(v7);
result = 0;
}
else
{
*(_DWORD *)a3 = v7;
*(_DWORD *)a4 = v9;
result = 1;
}
return result;
}
The TEA key is:
Code: Select all
const uint8_t key[] = { 0xF3, 0xED, 0xA4, 0xAE, 0x2A, 0x33, 0xF8, 0xAF, 0xB4, 0xDB, 0xA2, 0xB5, 0x22, 0xA0, 0x4B, 0x9B };
This will decrypt the files back to a GGData object.
-
- Posts: 250
- Joined: Sat Dec 27, 2014 8:49 pm
Re: Thimbleweed Park savegames
From there, the game then checks for the file marker:
If that matches it looks like it parses the data as a GGArray<GGString* >.
This last part looks like it potentially ties into Squirrel scripting though while parsing the string data back from the file and loading directly into the script engine.
Code: Select all
01 02 03 04
Code: Select all
int __cdecl sub_4C1EB0(int a1, int a2)
{
_BYTE *v3; // ecx
_DWORD *v4; // eax
int v5; // [esp+0h] [ebp-Ch]
int v6; // [esp+4h] [ebp-8h]
int v7; // [esp+8h] [ebp-4h]
if ( !a1 )
return 0;
if ( *(_DWORD *)(a1 + 20) > 4 )
{
v3 = *(_BYTE **)(a1 + 16);
if ( *v3 == 1 && v3[1] == 2 && v3[2] == 3 && v3[3] == 4 )
return sub_4C1F30((_DWORD *)a1, a2);
}
v4 = sub_456A10(a1);
v5 = 0;
v6 = 0;
v7 = 0;
if ( !v4 )
return 0;
return sub_4C1BC0((int)&v5, (int)v4, a2, 0);
}
If that matches it looks like it parses the data as a GGArray<GGString* >.
Code: Select all
int __cdecl sub_4C1F30(_DWORD *a1, int a2)
{
_DWORD *v2; // esi
int v3; // eax
int v4; // eax
_DWORD *v5; // esi
signed int v6; // ecx
char *v7; // eax
signed int v9; // eax
signed int v10; // eax
signed int v11; // eax
signed int v12; // eax
int v13; // eax
int v14; // ebx
char v15; // cl
int i; // eax
_DWORD *v17; // eax
_DWORD *v18; // edi
int v19; // ecx
int v20; // ST10_4
int v21; // esi
void **v22; // [esp+10h] [ebp-28h]
int v23; // [esp+14h] [ebp-24h]
int v24; // [esp+18h] [ebp-20h]
int v25; // [esp+1Ch] [ebp-1Ch]
int v26; // [esp+20h] [ebp-18h]
int v27; // [esp+24h] [ebp-14h]
int v28; // [esp+28h] [ebp-10h]
int v29; // [esp+34h] [ebp-4h]
v2 = (_DWORD *)dword_6D7440;
if ( dword_6D7440 )
{
v3 = *(_DWORD *)(dword_6D7440 + 8);
if ( v3 != -1000 )
*(_DWORD *)(dword_6D7440 + 8) = v3 - 1;
(*(void (__thiscall **)(_DWORD *))(*v2 + 8))(v2);
v4 = v2[2];
if ( v4 != -1000 && v4 <= 0 )
{
++dword_6E7754;
(*(void (__thiscall **)(_DWORD *, signed int))*v2)(v2, 1);
dword_6E7754 -= 2;
}
}
v5 = a1;
dword_6D7440 = 0;
if ( !a1 )
return 0;
v6 = a1[5];
if ( v6 > 4 )
{
v7 = (char *)a1[4];
if ( *v7 != 1 || v7[1] != 2 || v7[2] != 3 || v7[3] != 4 )
{
sub_4C2120("bad marker: %d,%d,%d,%d", *v7, *v7 + 1, *v7 + 2, *v7 + 3);
return 0;
}
}
v24 = 1;
v25 = 0;
v22 = &GGArray<GGString *>::`vftable';
v26 = 0;
v27 = 0;
v28 = 0;
v23 = 2;
v9 = a1[6];
v29 = 0;
if ( v9 < v6 )
a1[6] = v9 + 1;
v10 = v5[6];
if ( v10 < v6 )
v5[6] = v10 + 1;
v11 = v5[6];
if ( v11 < v6 )
v5[6] = v11 + 1;
v12 = v5[6];
if ( v12 < v6 )
v5[6] = v12 + 1;
sub_4C2870(v5);
v13 = sub_4C2870(v5);
v14 = v5[6];
v5[6] = v13;
if ( v13 >= v5[5] || (v15 = *(_BYTE *)(v13 + v5[4]), v5[6] = v13 + 1, v15 != 7) )
{
v21 = 0;
}
else
{
for ( i = sub_4C2870(v5); i != -1; i = sub_4C2870(v5) )
{
v17 = (_DWORD *)sub_4D00F0((void *)(v5[4] + i));
v18 = v17;
if ( v17 )
{
v19 = v17[2];
if ( v19 != -1000 )
v17[2] = v19 + 1;
(*(void (__thiscall **)(_DWORD *))(*v17 + 4))(v17);
a1 = v18;
sub_443D50(&v26, (unsigned int *)&a1);
}
}
v20 = a2;
v5[6] = v14;
v21 = sub_4C2770(v5, &v22, v20);
}
sub_417F50();
return v21;
}
This last part looks like it potentially ties into Squirrel scripting though while parsing the string data back from the file and loading directly into the script engine.
-
- Posts: 4
- Joined: Sat Apr 11, 2020 7:48 am
Re: Thimbleweed Park savegames
Wow I'm impressed by the quick answer.
I will have a look to the TEA encryption, thank you for your help.
I will have a look to the TEA encryption, thank you for your help.
-
- Posts: 250
- Joined: Sat Dec 27, 2014 8:49 pm
Re: Thimbleweed Park savegames
From the look of it, they 'serialize' the file in a manner that turns it into parts.
- A header with some basic information.
- A string index table holding all the lookups to the actual key/values.
- A string table holding the real string information.
- A header with some basic information.
- A string index table holding all the lookups to the actual key/values.
- A string table holding the real string information.
-
- Posts: 250
- Joined: Sat Dec 27, 2014 8:49 pm
Re: Thimbleweed Park savegames
Looks like you guys have the rest of what is needed done in your repo here:
https://github.com/scemino/engge/blob/m ... GGPack.hpp
For me to get this working with the save file I had to adjust a few things:
- The GGPack::readPack function needs to just directly assume the file is already decrypted after the above steps/info.
- The sig is immediately valid as the first 4 bytes due to the above.
- readHash needs to be adjusted for n_pairs == 0, this seems to be a valid case in save files so ignore throwing an exception there.
- readPack then needs to be adjusted to basically just return everything instead of just file entries.
Example of it working for me:
https://github.com/scemino/engge/blob/m ... GGPack.hpp
For me to get this working with the save file I had to adjust a few things:
- The GGPack::readPack function needs to just directly assume the file is already decrypted after the above steps/info.
- The sig is immediately valid as the first 4 bytes due to the above.
- readHash needs to be adjusted for n_pairs == 0, this seems to be a valid case in save files so ignore throwing an exception there.
- readPack then needs to be adjusted to basically just return everything instead of just file entries.
Example of it working for me: