Thimbleweed Park savegames

Reading, editing and everything related to the files created by games to contain savegames and configurations
scemino
Posts: 4
Joined: Sat Apr 11, 2020 7:48 am

Thimbleweed Park savegames

Post by scemino »

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
scemino
Posts: 4
Joined: Sat Apr 11, 2020 7:48 am

Re: Thimbleweed Park savegames

Post by scemino »

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

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
}
atom0s
Posts: 250
Joined: Sat Dec 27, 2014 8:49 pm

Re: Thimbleweed Park savegames

Post by atom0s »

We would need some saved game files and generally the game exe / dll's (if any are used) to be able to assist.
scemino
Posts: 4
Joined: Sat Apr 11, 2020 7:48 am

Re: Thimbleweed Park savegames

Post by scemino »

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/
atom0s
Posts: 250
Joined: Sat Dec 27, 2014 8:49 pm

Re: Thimbleweed Park savegames

Post by atom0s »

The game is using TEA encryption for the saved files with some extra checking afterward.

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.
atom0s
Posts: 250
Joined: Sat Dec 27, 2014 8:49 pm

Re: Thimbleweed Park savegames

Post by atom0s »

From there, the game then checks for the file marker:

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.
scemino
Posts: 4
Joined: Sat Apr 11, 2020 7:48 am

Re: Thimbleweed Park savegames

Post by scemino »

:o Wow I'm impressed by the quick answer.
I will have a look to the TEA encryption, thank you for your help.
atom0s
Posts: 250
Joined: Sat Dec 27, 2014 8:49 pm

Re: Thimbleweed Park savegames

Post by atom0s »

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.
atom0s
Posts: 250
Joined: Sat Dec 27, 2014 8:49 pm

Re: Thimbleweed Park savegames

Post by atom0s »

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:
Image