Git Product home page Git Product logo

Comments (43)

atom0s avatar atom0s commented on May 30, 2024 1

Sure, I just ripped the funcs directly from the game rather than reimplement them manually:

void __cdecl sub_4D9710(DWORD *a1, signed int a2, int a3)
{
    int v3; // ecx
    DWORD *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
    DWORD *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);
    }
}

int32_t __cdecl main(int32_t argc, char* argv[])
{
    FILE* f = nullptr;
    fopen_s(&f, u8"C:\\Users\\atom0s\\Desktop\\teadecpp\\Debug\\Savegame1.save", u8"rb");
    fseek(f, 0, SEEK_END);
    const auto size = ftell(f);
    fseek(f, 0, SEEK_SET);

    std::vector<uint8_t> data(size, u8'\0');
    fread(data.data(), 1, size, f);
    fclose(f);

    const int32_t decSize = size / -4;
    const uint8_t key[] = { 0xF3, 0xED, 0xA4, 0xAE, 0x2A, 0x33, 0xF8, 0xAF, 0xB4, 0xDB, 0xA2, 0xB5, 0x22, 0xA0, 0x4B, 0x9B };

    sub_4D9710((DWORD*)&data[0], decSize, (int32_t)&key);

    fopen_s(&f, u8"C:\\Users\\atom0s\\Desktop\\teadecpp\\Debug\\Savegame1.save.dump", u8"wb");
    fwrite(data.data(), 1, data.size(), f);
    fclose(f);

    return 0;
}

Quick throw together just to test the decryption.

from engge.

scemino avatar scemino commented on May 30, 2024 1

from engge.

Mac1512 avatar Mac1512 commented on May 30, 2024 1

I have tested the commit: Clean crypto code (7f9cc32) on windows and it works correctly.

I comment it, because I think you should know that it works well :)

from engge.

scemino avatar scemino commented on May 30, 2024

For this task, any help will be welcome, I need to figure out how savegame is structured.

from engge.

neuromancer avatar neuromancer commented on May 30, 2024

you want the savegames to be compatible with the original implementation?

from engge.

scemino avatar scemino commented on May 30, 2024

It would be awesome no?

from engge.

neuromancer avatar neuromancer commented on May 30, 2024

There is a general explanation of save games here: https://blog.thimbleweedpark.com/savegame (you probably already know about this, but it's a good idea to compile this information somewhere..). Also, extra saves are here: https://www.gog.com/forum/general/thimbleweed_park_save_game_request

from engge.

scemino avatar scemino commented on May 30, 2024

Yes I'm aware of this great blog, it's a gold mine. But as you said it's a good idea to gather all information here.

from engge.

scemino avatar scemino commented on May 30, 2024

If anybody knows someone who can work on this task I will be very pleased.
This subject becomes critical, it's kind of hard now to go further without having savegames implemented.
I contacted on Sep 1st, 2019 mstr- by email, but he didn't answer me :(

from engge.

Mac1512 avatar Mac1512 commented on May 30, 2024

Hello, have you asked Luigi Auriemma for help, or has anyone in the following forums?

https://www.zenhax.com

or

https://forum.xentax.com/

Maybe there, someone is able to decrypt the saved games.

Another thing that can be done is to try to follow the steps described by Ron Gilbert and try to record the variables described in the Json file, until a saved game is loaded. And then if at any time someone decrypts the originals compare and adapt to the original.

from engge.

scemino avatar scemino commented on May 30, 2024

Good idea, thank you, I posted a message on the forum, we will see...

from engge.

Mac1512 avatar Mac1512 commented on May 30, 2024

Yes, hopefully someone can help.

from engge.

atom0s avatar atom0s commented on May 30, 2024

Hey guys, I helped scemino over on Zenhax but figured I'd share the info I reversed here too to keep it with the actual project so it doesn't get lost on a random site elsewhere.

The game encrypts the save files using TEA encryption. The encryption key used is:

const uint8_t key[] = { 0xF3, 0xED, 0xA4, 0xAE, 0x2A, 0x33, 0xF8, 0xAF, 0xB4, 0xDB, 0xA2, 0xB5, 0x22, 0xA0, 0x4B, 0x9B };

The decryption methods are:

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;
}

This will decrypt the save file back to a raw 'GGData' object, similar to what you guys have as a GGPack in your code here:
https://github.com/scemino/engge/blob/master/src/Parsers/GGPack.cpp

The file header is checked for a specific pattern 0x01020304:

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);
}```

Then parsed if matched using:
```cpp
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 is where the GGPack style reading is happening. You can adjust the current GGPack in a separate app to test with by editing:

  • 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.

Once done, the current GGPack code can read the save file:

fv74765

from engge.

scemino avatar scemino commented on May 30, 2024

That's awesome @atom0s !!
By chance, can you share your decrypt cpp source code ?
Thanks anyway.

from engge.

Mac1512 avatar Mac1512 commented on May 30, 2024

Very good job atom0s !!! It is impressive how quickly you have solved it.

Thank you very much for helping scemino and for this great project that he is carrying out.

I congratulate you again.

Cheers :)

from engge.

Mac1512 avatar Mac1512 commented on May 30, 2024

There is a file called "Save.dat", which occupies 216 bytes. This file is created when you exit the program, whether or not there are saved games.

This file cannot be decrypted with the previously posted function.

atom0s, would you mind taking a look at this one too?

Thank you very much

Cheers

from engge.

atom0s avatar atom0s commented on May 30, 2024

For the 'Save.dat' file, the key is different. The rest of the functionality is the same though as I outlined above. The decryption key for that file is:

    const uint8_t key[] = { 0x93, 0x9D, 0xAB, 0x2A, 0x2A, 0x56, 0xF8, 0xAF, 0xB4, 0xDB, 0xA2, 0xB5, 0x22, 0xA3, 0x4B, 0x2B };

That one decrypts to text without needing to be further processed.

from engge.

atom0s avatar atom0s commented on May 30, 2024

@scemino I saw you requested the encryption function the game uses. It's the same as the above stuff but in reverse order. Here is how the client does it. The encryption itself is the same function from above for TEA/XTEA:

char __cdecl sub_4D94A0(void *Src, size_t Size, int a3, int a4, int a5)
{
  int v5; // eax
  int v6; // eax
  int v7; // ebx
  void *v8; // eax
  _DWORD **v9; // edi
  signed int v10; // ecx
  signed int v11; // edx
  int v12; // eax
  __int16 v13; // cx
  int i; // eax
  int v15; // ecx
  signed int v17; // [esp+0h] [ebp-4h]

  if ( !Src || !a5 )
    return 0;
  if ( byte_6E803C )
  {
    v5 = dword_6E8040;
  }
  else
  {
    byte_6E803C = 1;
    v5 = time64(0);
  }
  dword_6E8040 = v5 + 1;
  v6 = (signed int)(Size + 9) % 8;
  v7 = 8 - v6;
  v17 = 8 - v6 + Size + 9;
  v8 = malloc(v17);
  v9 = (_DWORD **)a3;
  *(_DWORD *)a3 = v8;
  if ( v8 )
    memcpy(v8, Src, Size);
  v10 = 0;
  v11 = 0x6583463;
  if ( (signed int)Size > 0 )
  {
    do
    {
      v12 = *(unsigned __int8 *)(*(_DWORD *)a3 + v10++);
      v11 += v12;
    }
    while ( v10 < (signed int)Size );
    v9 = (_DWORD **)a3;
  }
  *(_WORD *)((char *)*v9 + Size) = v11;
  *((_BYTE *)*v9 + Size + 2) = BYTE2(v11);
  v13 = dword_6E8040;
  *((_BYTE *)*v9 + Size + 3) = HIBYTE(v11);
  *(_WORD *)((char *)*v9 + Size + 4) = v13;
  *(_WORD *)((char *)*v9 + Size + 6) = HIWORD(dword_6E8040);
  *((_BYTE *)*v9 + Size + 8) = v7;
  for ( i = 0; i < v7; *(_BYTE *)(v15 + Size + 9) = v7 )
    v15 = (int)*v9 + i++;
  sub_4D9710(*v9, v17 / 4, a5);
  *(_DWORD *)a4 = v17;
  return 1;
}

from engge.

scemino avatar scemino commented on May 30, 2024

For the 'Save.dat' file, the key is different. The rest of the functionality is the same though as I outlined above. The decryption key for that file is:

    const uint8_t key[] = { 0x93, 0x9D, 0xAB, 0x2A, 0x2A, 0x56, 0xF8, 0xAF, 0xB4, 0xDB, 0xA2, 0xB5, 0x22, 0xA3, 0x4B, 0x2B };

That one decrypts to text without needing to be further processed.

Great, thank you @atom0s, it works fine, yes it's a text file including some stats and achievements.
I have a lot of work now before supporting savegames 😄

from engge.

scemino avatar scemino commented on May 30, 2024

@scemino I saw you requested the encryption function the game uses. It's the same as the above stuff but in reverse order. Here is how the client does it. The encryption itself is the same function from above for TEA/XTEA:

char __cdecl sub_4D94A0(void *Src, size_t Size, int a3, int a4, int a5)
{
  int v5; // eax
  int v6; // eax
  int v7; // ebx
  void *v8; // eax
  _DWORD **v9; // edi
  signed int v10; // ecx
  signed int v11; // edx
  int v12; // eax
  __int16 v13; // cx
  int i; // eax
  int v15; // ecx
  signed int v17; // [esp+0h] [ebp-4h]

  if ( !Src || !a5 )
    return 0;
  if ( byte_6E803C )
  {
    v5 = dword_6E8040;
  }
  else
  {
    byte_6E803C = 1;
    v5 = time64(0);
  }
  dword_6E8040 = v5 + 1;
  v6 = (signed int)(Size + 9) % 8;
  v7 = 8 - v6;
  v17 = 8 - v6 + Size + 9;
  v8 = malloc(v17);
  v9 = (_DWORD **)a3;
  *(_DWORD *)a3 = v8;
  if ( v8 )
    memcpy(v8, Src, Size);
  v10 = 0;
  v11 = 0x6583463;
  if ( (signed int)Size > 0 )
  {
    do
    {
      v12 = *(unsigned __int8 *)(*(_DWORD *)a3 + v10++);
      v11 += v12;
    }
    while ( v10 < (signed int)Size );
    v9 = (_DWORD **)a3;
  }
  *(_WORD *)((char *)*v9 + Size) = v11;
  *((_BYTE *)*v9 + Size + 2) = BYTE2(v11);
  v13 = dword_6E8040;
  *((_BYTE *)*v9 + Size + 3) = HIBYTE(v11);
  *(_WORD *)((char *)*v9 + Size + 4) = v13;
  *(_WORD *)((char *)*v9 + Size + 6) = HIWORD(dword_6E8040);
  *((_BYTE *)*v9 + Size + 8) = v7;
  for ( i = 0; i < v7; *(_BYTE *)(v15 + Size + 9) = v7 )
    v15 = (int)*v9 + i++;
  sub_4D9710(*v9, v17 / 4, a5);
  *(_DWORD *)a4 = v17;
  return 1;
}

Yes I deleted my message, because I figured it out after, if I call the same method with a positive content size (called 'a2') then the method encrypts the data.

from engge.

Mac1512 avatar Mac1512 commented on May 30, 2024

For the 'Save.dat' file, the key is different. The rest of the functionality is the same though as I outlined above. The decryption key for that file is:

    const uint8_t key[] = { 0x93, 0x9D, 0xAB, 0x2A, 0x2A, 0x56, 0xF8, 0xAF, 0xB4, 0xDB, 0xA2, 0xB5, 0x22, 0xA3, 0x4B, 0x2B };

That one decrypts to text without needing to be further processed.

Thank you very much!! It works great :)

from engge.

Mac1512 avatar Mac1512 commented on May 30, 2024

@scemino I saw you requested the encryption function the game uses. It's the same as the above stuff but in reverse order. Here is how the client does it. The encryption itself is the same function from above for TEA/XTEA:

char __cdecl sub_4D94A0(void *Src, size_t Size, int a3, int a4, int a5)
{
  int v5; // eax
  int v6; // eax
  int v7; // ebx
  void *v8; // eax
  _DWORD **v9; // edi
  signed int v10; // ecx
  signed int v11; // edx
  int v12; // eax
  __int16 v13; // cx
  int i; // eax
  int v15; // ecx
  signed int v17; // [esp+0h] [ebp-4h]

  if ( !Src || !a5 )
    return 0;
  if ( byte_6E803C )
  {
    v5 = dword_6E8040;
  }
  else
  {
    byte_6E803C = 1;
    v5 = time64(0);
  }
  dword_6E8040 = v5 + 1;
  v6 = (signed int)(Size + 9) % 8;
  v7 = 8 - v6;
  v17 = 8 - v6 + Size + 9;
  v8 = malloc(v17);
  v9 = (_DWORD **)a3;
  *(_DWORD *)a3 = v8;
  if ( v8 )
    memcpy(v8, Src, Size);
  v10 = 0;
  v11 = 0x6583463;
  if ( (signed int)Size > 0 )
  {
    do
    {
      v12 = *(unsigned __int8 *)(*(_DWORD *)a3 + v10++);
      v11 += v12;
    }
    while ( v10 < (signed int)Size );
    v9 = (_DWORD **)a3;
  }
  *(_WORD *)((char *)*v9 + Size) = v11;
  *((_BYTE *)*v9 + Size + 2) = BYTE2(v11);
  v13 = dword_6E8040;
  *((_BYTE *)*v9 + Size + 3) = HIBYTE(v11);
  *(_WORD *)((char *)*v9 + Size + 4) = v13;
  *(_WORD *)((char *)*v9 + Size + 6) = HIWORD(dword_6E8040);
  *((_BYTE *)*v9 + Size + 8) = v7;
  for ( i = 0; i < v7; *(_BYTE *)(v15 + Size + 9) = v7 )
    v15 = (int)*v9 + i++;
  sub_4D9710(*v9, v17 / 4, a5);
  *(_DWORD *)a4 = v17;
  return 1;
}

Hello atom0s

Although the important thing here is that scemino understands the encryption process. I would also like to understand it, so that I can help scemino in what I can. And I don't know how to do it.

I see this encryption function and there are things I don't understand. And others that perhaps if you have understood them, therefore:

  1. The parameters that the function receives, I think they would be the following, correct me if I'm wrong:

a3 -> target data
a4 -> target data size
a5 -> encryption key

  1. I see two variables:

byte_6E803C
dword_6E8040

These are not defined within the body of the function:

char __cdecl sub_4D94A0 (void * Src, size_t Size, int a3, int a4, int a5)

What do they contain or how would they be defined?

Could you pass a real encryption example like the one you passed for decryption?

In any case, thank you very much for your help.

Cheers

from engge.

scemino avatar scemino commented on May 30, 2024

@scemino I saw you requested the encryption function the game uses. It's the same as the above stuff but in reverse order. Here is how the client does it. The encryption itself is the same function from above for TEA/XTEA:

char __cdecl sub_4D94A0(void *Src, size_t Size, int a3, int a4, int a5)
{
  int v5; // eax
  int v6; // eax
  int v7; // ebx
  void *v8; // eax
  _DWORD **v9; // edi
  signed int v10; // ecx
  signed int v11; // edx
  int v12; // eax
  __int16 v13; // cx
  int i; // eax
  int v15; // ecx
  signed int v17; // [esp+0h] [ebp-4h]

  if ( !Src || !a5 )
    return 0;
  if ( byte_6E803C )
  {
    v5 = dword_6E8040;
  }
  else
  {
    byte_6E803C = 1;
    v5 = time64(0);
  }
  dword_6E8040 = v5 + 1;
  v6 = (signed int)(Size + 9) % 8;
  v7 = 8 - v6;
  v17 = 8 - v6 + Size + 9;
  v8 = malloc(v17);
  v9 = (_DWORD **)a3;
  *(_DWORD *)a3 = v8;
  if ( v8 )
    memcpy(v8, Src, Size);
  v10 = 0;
  v11 = 0x6583463;
  if ( (signed int)Size > 0 )
  {
    do
    {
      v12 = *(unsigned __int8 *)(*(_DWORD *)a3 + v10++);
      v11 += v12;
    }
    while ( v10 < (signed int)Size );
    v9 = (_DWORD **)a3;
  }
  *(_WORD *)((char *)*v9 + Size) = v11;
  *((_BYTE *)*v9 + Size + 2) = BYTE2(v11);
  v13 = dword_6E8040;
  *((_BYTE *)*v9 + Size + 3) = HIBYTE(v11);
  *(_WORD *)((char *)*v9 + Size + 4) = v13;
  *(_WORD *)((char *)*v9 + Size + 6) = HIWORD(dword_6E8040);
  *((_BYTE *)*v9 + Size + 8) = v7;
  for ( i = 0; i < v7; *(_BYTE *)(v15 + Size + 9) = v7 )
    v15 = (int)*v9 + i++;
  sub_4D9710(*v9, v17 / 4, a5);
  *(_DWORD *)a4 = v17;
  return 1;
}

Hello atom0s

Although the important thing here is that scemino understands the encryption process. I would also like to understand it, so that I can help scemino in what I can. And I don't know how to do it.

I see this encryption function and there are things I don't understand. And others that perhaps if you have understood them, therefore:

  1. The parameters that the function receives, I think they would be the following, correct me if I'm wrong:

a3 -> target data a4 -> target data size a5 -> encryption key

  1. I see two variables:

byte_6E803C dword_6E8040

These are not defined within the body of the function:

char __cdecl sub_4D94A0 (void * Src, size_t Size, int a3, int a4, int a5)

What do they contain or how would they be defined?

Could you pass a real encryption example like the one you passed for decryption?

In any case, thank you very much for your help.

Cheers

No need, the encryption works for me as well.
But thanks anyway.
I have all the information I need for the moment.

from engge.

atom0s avatar atom0s commented on May 30, 2024

Hello atom0s

Although the important thing here is that scemino understands the encryption process. I would also like to understand it, so that I can help scemino in what I can. And I don't know how to do it.

I see this encryption function and there are things I don't understand. And others that perhaps if you have understood them, therefore:

  1. The parameters that the function receives, I think they would be the following, correct me if I'm wrong:

a3 -> target data a4 -> target data size a5 -> encryption key

  1. I see two variables:

byte_6E803C dword_6E8040

These are not defined within the body of the function:

char __cdecl sub_4D94A0 (void * Src, size_t Size, int a3, int a4, int a5)

What do they contain or how would they be defined?

Could you pass a real encryption example like the one you passed for decryption?

In any case, thank you very much for your help.

Cheers

Sure.

char __cdecl sub_4D94A0(void *Src, size_t Size, int a3, int a4, int a5)

The parameters here are:

  • Src - Is the input data to be processed.
  • Size - Is the size of the input data.
  • a3 - Output buffer pointer to hold the processed data.
  • a4 - Output buffer size.
  • a5 - The encryption key pointer.

byte_6E803C is just used as a flag to determine if something has happened yet. In this case, it's used to tell if a 'baseline' timestamp has been created to be used to timestamp file data. This is added to the end of the file data as a 'footer' for the data. The end chunk before the encryption is happening is where that footer is being created:

  v11 = 0x6583463;
  if ( (signed int)Size > 0 )
  {
    do
    {
      v12 = *(unsigned __int8 *)(*(_DWORD *)a3 + v10++);
      v11 += v12;
    }
    while ( v10 < (signed int)Size );
    v9 = (_DWORD **)a3;
  }
  *(_WORD *)((char *)*v9 + Size) = v11;
  *((_BYTE *)*v9 + Size + 2) = BYTE2(v11);
  v13 = dword_6E8040;
  *((_BYTE *)*v9 + Size + 3) = HIBYTE(v11);
  *(_WORD *)((char *)*v9 + Size + 4) = v13;
  *(_WORD *)((char *)*v9 + Size + 6) = HIWORD(dword_6E8040);
  *((_BYTE *)*v9 + Size + 8) = v7;
  for ( i = 0; i < v7; *(_BYTE *)(v15 + Size + 9) = v7 )
    v15 = (int)*v9 + i++;

The first part (the while loop chunk) is creating a 'hash'-like number based on the data. Then that is written to this header as a means to validate the data for being tampered with.

dword_6E8040 is just the timestamp counter used. That's just initialized with time64(0) then incremented each time its used, by 1.

The way the overall encryption handler works is based on the size passed to it.

void __cdecl sub_4D9710(_DWORD *a1, signed int a2, int a3)

a2 handles the size of the data, which the function uses to determine if you are trying to encrypt or decrypt the data.

// Decryption uses negative size values..
sub_4D9710(v7, (signed int)Size / -4, a5);

// Encryption uses positive size values..
sub_4D9710(*v9, v17 / 4, a5);

Using the example decryption code I gave above you can recreate the encryption in reverse as needed. You can calculate the hash using:

int32_t v10 = 0;
int32_t v11 = 0x6583463;
int32_t v12 = 0;

do
{
    v12 = *(uint8_t*)&data[v10++];
    v11 += v12;
} while (v10 < size - 16);

// v11 now contains the hash that will be put in the footer..

The timestamp part doesn't really matter and is solely used for visuals from the look of it. The decryption function doesn't check it for anything, so it is just used to see the date of the save in-game.

The last 8 bytes are just repeated as a marker from the look of things as well. Decryption doesn't seem to check that either.

for ( i = 0; i < v7; *(_BYTE *)(v15 + Size + 9) = v7 )
    v15 = (int)*v9 + i++;

v7 here is calculated from the size:

v6 = (signed int)(Size + 9) % 8;
v7 = 8 - v6;

So if the data size (without the footer) is: 0x7A120

Then we get: 8 - ((0x7A120 + 9) % 8) or 0x07

So then our footer would look like this:

80 31 23 07 29 C1 9A 5A 07 07 07 07 07 07 07 07

80 31 23 07 - The 'hash' of the data.
29 C1 9A 5A - The 'timestamp' of the data.
07 07 07 07 07 07 07 07 - The repeated last byte.

After this is calculated the encryption function is called. For decryption, the reverse is done where the data is decrypted, the footer is validated, then the data is processed. The decryption function only seems to validate the hash though, the timestamp and repeated 8 bytes are ignored from the look of things.

from engge.

Mac1512 avatar Mac1512 commented on May 30, 2024

Hello atom0s
Although the important thing here is that scemino understands the encryption process. I would also like to understand it, so that I can help scemino in what I can. And I don't know how to do it.
I see this encryption function and there are things I don't understand. And others that perhaps if you have understood them, therefore:

  1. The parameters that the function receives, I think they would be the following, correct me if I'm wrong:

a3 -> target data a4 -> target data size a5 -> encryption key

  1. I see two variables:

byte_6E803C dword_6E8040
These are not defined within the body of the function:
char __cdecl sub_4D94A0 (void * Src, size_t Size, int a3, int a4, int a5)
What do they contain or how would they be defined?
Could you pass a real encryption example like the one you passed for decryption?
In any case, thank you very much for your help.
Cheers

Sure.

char __cdecl sub_4D94A0(void *Src, size_t Size, int a3, int a4, int a5)

The parameters here are:

* Src - Is the input data to be processed.

* Size - Is the size of the input data.

* a3 - Output buffer pointer to hold the processed data.

* a4 - Output buffer size.

* a5 - The encryption key pointer.

byte_6E803C is just used as a flag to determine if something has happened yet. In this case, it's used to tell if a 'baseline' timestamp has been created to be used to timestamp file data. This is added to the end of the file data as a 'footer' for the data. The end chunk before the encryption is happening is where that footer is being created:

  v11 = 0x6583463;
  if ( (signed int)Size > 0 )
  {
    do
    {
      v12 = *(unsigned __int8 *)(*(_DWORD *)a3 + v10++);
      v11 += v12;
    }
    while ( v10 < (signed int)Size );
    v9 = (_DWORD **)a3;
  }
  *(_WORD *)((char *)*v9 + Size) = v11;
  *((_BYTE *)*v9 + Size + 2) = BYTE2(v11);
  v13 = dword_6E8040;
  *((_BYTE *)*v9 + Size + 3) = HIBYTE(v11);
  *(_WORD *)((char *)*v9 + Size + 4) = v13;
  *(_WORD *)((char *)*v9 + Size + 6) = HIWORD(dword_6E8040);
  *((_BYTE *)*v9 + Size + 8) = v7;
  for ( i = 0; i < v7; *(_BYTE *)(v15 + Size + 9) = v7 )
    v15 = (int)*v9 + i++;

The first part (the while loop chunk) is creating a 'hash'-like number based on the data. Then that is written to this header as a means to validate the data for being tampered with.

dword_6E8040 is just the timestamp counter used. That's just initialized with time64(0) then incremented each time its used, by 1.

The way the overall encryption handler works is based on the size passed to it.

void __cdecl sub_4D9710(_DWORD *a1, signed int a2, int a3)

a2 handles the size of the data, which the function uses to determine if you are trying to encrypt or decrypt the data.

// Decryption uses negative size values..
sub_4D9710(v7, (signed int)Size / -4, a5);

// Encryption uses positive size values..
sub_4D9710(*v9, v17 / 4, a5);

Using the example decryption code I gave above you can recreate the encryption in reverse as needed. You can calculate the hash using:

int32_t v10 = 0;
int32_t v11 = 0x6583463;
int32_t v12 = 0;

do
{
    v12 = *(uint8_t*)&data[v10++];
    v11 += v12;
} while (v10 < size - 16);

// v11 now contains the hash that will be put in the footer..

The timestamp part doesn't really matter and is solely used for visuals from the look of it. The decryption function doesn't check it for anything, so it is just used to see the date of the save in-game.

The last 8 bytes are just repeated as a marker from the look of things as well. Decryption doesn't seem to check that either.

for ( i = 0; i < v7; *(_BYTE *)(v15 + Size + 9) = v7 )
    v15 = (int)*v9 + i++;

v7 here is calculated from the size:

v6 = (signed int)(Size + 9) % 8;
v7 = 8 - v6;

So if the data size (without the footer) is: 0x7A120

Then we get: 8 - ((0x7A120 + 9) % 8) or 0x07

So then our footer would look like this:

80 31 23 07 29 C1 9A 5A 07 07 07 07 07 07 07 07

80 31 23 07 - The 'hash' of the data.
29 C1 9A 5A - The 'timestamp' of the data.
07 07 07 07 07 07 07 07 - The repeated last byte.

After this is calculated the encryption function is called. For decryption, the reverse is done where the data is decrypted, the footer is validated, then the data is processed. The decryption function only seems to validate the hash though, the timestamp and repeated 8 bytes are ignored from the look of things.

Now it has become clear to me ... it has been a magnificent explanation :)

I'm going to put it into practice.

Thank you very much atom0s !!!

from engge.

scemino avatar scemino commented on May 30, 2024

Oh ok. Sorry @Mac1512 you were right, this is something very interesting.
Good work @atom0s

from engge.

Mac1512 avatar Mac1512 commented on May 30, 2024

Oh ok. Sorry @Mac1512 you were right, this is something very interesting.
Good work @atom0s

No problem @scemino :)

The truth is that the information that atomos is providing is very useful and very interesting.

In fact, I had seen those data when decrypting the Saves.dat file and I have observed later, after the clarification given by atom0s, that it is also found in the saved games.

At first I didn't know what that data was. Now I see that they are a few extra bytes, to include information, like a footer :)

the timestamp saved here is the same as the one saved in the "savetime" field of the decrypted json file (of the saved games), adding a 1.

Encryption still doesn't work for me, when I compiled it threw me an exception, for lack of time I haven't been able to test more. I will make some modifications to make it work, as much as possible

again thank you very much @atom0s for your clarifications.

Cheers

Edited:

I have had to make a small modification since there are no certain macros, such as BYTE2 ... Now I can say that it works perfect :)

from engge.

neuromancer avatar neuromancer commented on May 30, 2024

Wow, is this feature ready for testing?

from engge.

Mac1512 avatar Mac1512 commented on May 30, 2024

I didn't get there in time to let you know before you released release 0.5.0.

At least in windows, there is a problem with saves:

[2020-04-28 08: 29: 47.101] [log] [warning] Invalid savegame: Savegame1.save
[2020-04-28 08: 29: 47.101] [log] [error] Sorry, an error occurred: This is not an hashtable

Although I have not been able to test it, I am convinced that it does not save the hash well, that happened to me, when I compiled the function that I provide atom0s; the calculated hash value is correct, but the saving was incorrect when storing the int value, making it byte by byte. Besides, you have to discount the space that this hash occupies plus the saving time and repeated characters ... as explained by atom0s. Since saved games take up more than they should.

Saves Original = 500016 bytes
Saves engge = 502056 bytes

I see there is a difference of 2040 bytes.

So it makes me think that there is a mistake when coding.

Due to lack of time, I cannot yet decode the saves and see what is specifically being stored and thus be able to give you more accurate data.

If I can I will prove it today in the afternoon.

Cheers

from engge.

scemino avatar scemino commented on May 30, 2024

Ok I removed the release.
Can you tell me what did you do exactly ? And can you send me your savegame ?
This is odd, I saved a game and I have exactly 500 016 bytes with engge, and I can load it again (tested on macOS).
Thank you

from engge.

Mac1512 avatar Mac1512 commented on May 30, 2024

Hello,

You will see three folders, one of them is called:

With 0.5.0 github

In this are the save, from the version you uploaded, you will see that it has a size of 501922 bytes.

There are two other folders called 1 and 2, these are created from commit 0f09c4f, which corresponds to version release 0.5.0

the saves occupy in this case: 501997 bytes and 501924 bytes, respectively.

From what I see every time the saved value changes ...

The saved game that I commented on the issue, I overwritten it by mistake, but from what I see it does not matter, since the data varies in all.

The proof of this I do as follows:

I start the program without the engge.nut file, so I start a game from scratch, and the first time the game is auto-saved, I quit the game (by the way the icons on the upper right do not appear to be able to quit the game, what I look for the calculation and the menu to exit appears ...), when starting again engge in the part of verifying the game, it closes and the error message appears in the log (the start screen is not seen).

Today I don't have time to try anything else ... I hope this can help you.

I pass the link with the saves

http://www.mediafire.com/file/e8dle3bh98uygq4/0.5.0_save_fail.7z/file

from engge.

scemino avatar scemino commented on May 30, 2024

Thank @Mac1512 I got the same errors with your given savegames.
I did the exact same things, I started a new game, I have the same issue: it is not possible from that point to go to the settings menu (gear icon), but the savegame is correct and I can load it if I restart engge.
I compared your json savegame to mine, and it's almost the same (except some values due to the random number generator and the language english vs spanish).
So I started once again in spanish, but It still succeeds, and the json savegames are almost identical.
I suppose it comes from the encode/decode method which must have a different behavior on Windows.

So here is my proposition, I try to change this method to make it more cross-platform, I create a commit and you will tell me if it works for you @Mac1512
Anyway thank you for your feedback, it's really helpful.
You are a great Windows and spanish tester ;)

from engge.

Mac1512 avatar Mac1512 commented on May 30, 2024

Thank you for your comments :)

I do it to learn and I would like this project to come to fruition xD

I have tried the last commit (fa36c40) and same error.

I pass you a link to mediafire, inside there are two compressed files, in one are the logs, one of them is from the beginning when the game is auto saved and another log of the failure. I send you the saved game of this test too.

In the other file, the source code (main.cpp) of the modification I made, of the functions of atom0s, goes so that the encoding in windows would work. with this come the following files:

Save.dat -> original Save.dat (216 bytes)
Save.json -> Decoded Save.dat (216 bytes)
Savepre.json -> Save.json without time index, hash ... (203 bytes)
Save.enc -> Result of encoding Savepre.json with the main.cpp that I passed you, is exactly the same as Save.dat

For convenience, I pass you the adapted main.cpp to generate a file equal to Save.dat (216 bytes). But the function is exactly the same as for saved games, only that they are in the ggpack format, and I use an intermediate program to pass them from json to ggpack.

This main.cpp can be improved, but I made it functional and left it like that xD

I hope it works for you and here I am to try anything on windows, within the possibilities of time

The link is as follows:

http://www.mediafire.com/file/z7eg9lep0iwvhjw/save_plus_encode_windows.7z/file

Cheers

from engge.

Mac1512 avatar Mac1512 commented on May 30, 2024

I have been doing some tests and I think that the failure may be in the order in which linux or mac store the data, perhaps it is big-endian and windows on the contrary little endian ...

Although I have not looked at the functions you use in engge to save saved games, when I tried to decode the saves generated by engge, with my decoding function for windows, even though they take up more than normal, that is not why it is due, the data I get is illegible, that is, I don't get the decoded ggpack file that I should get.

With the original saves, using my decoding function for windows, it does it right, I get the decoded ggpack file.

I pass you my original save, without decoding and decoding, are the files:

Savegame_orig.save -> without decoding
Savegame_orig.dec -> decoded

and the files:

Savegame1.save -> without decoding, commit (fa36c40)

Savegame1.dec -> Decoded (Unreadable Data)

Savegame2.save -> without decoding, commit (0f09c4f)

Savegame2.dec -> Decoded (Unreadable Data)

Download link:

http://www.mediafire.com/file/7jctn9vajhz5wfm/twp_saves_decode_fail.7z/file

from engge.

scemino avatar scemino commented on May 30, 2024

I can't read Savegame1.save and Savegame2.save, I'm able to read Savegame_orig.save.
I have to check on Windows, it will take me more time :(

from engge.

scemino avatar scemino commented on May 30, 2024

OK I found it, I changed my commit, it should be good now with the the commit aa672e2
Can you confirm this @Mac1512 please ?
It it's ok for you I will release the version v0.5.0-alpha

from engge.

Mac1512 avatar Mac1512 commented on May 30, 2024

Wow, now it works great :)

Well, I have been testing and the saved games are going well, in fact I have tried original and saved games directly by engge.

About the 0.5.0 release, because of the saved games if you can launch it ... although I see a series of errors that you should raise whether to solve them before releasing the 0.5.0 release or later for a 0.6.0 release.

I detail some of them, and depending on the decision you make, they put themselves in new issues:

  1. The icons on the right side do not appear ... (although this does not happen in all parts)

Icons are missing

  1. Reyes appears with a hat if the game is loaded from the beginning, although this hat disappears as he speaks.

reyes_hat

-3. In the dialogue between Ray and Reyes, if Ray is the one who talks to Reyes, the options for dialogue do not appear, but if it is Reyes who talks to Ray if ...

I suppose there are more ... but in what little I have tried, I have at least seen these.

from engge.

neuromancer avatar neuromancer commented on May 30, 2024

Reyes appears with a hat if the game is loaded from the beginning, although this hat disappears as he speaks.

Why the hat?

from engge.

Mac1512 avatar Mac1512 commented on May 30, 2024

I think it must be an error when storing any of the variables of the saved game. I guess the costume is not setting up correctly.

from engge.

scemino avatar scemino commented on May 30, 2024

from engge.

Mac1512 avatar Mac1512 commented on May 30, 2024

It seems perfect :)

from engge.

scemino avatar scemino commented on May 30, 2024

-3. In the dialogue between Ray and Reyes, if Ray is the one who talks to Reyes, the options for dialogue do not appear, but if it is Reyes who talks to Ray if ...

What do you mean exactly ? if I select Ray, I can't talk to Reyes, right ?

from engge.

Mac1512 avatar Mac1512 commented on May 30, 2024

Yes, exactly

In fact the action gets to be executed, but the dialogue texts do not appear if it is Ray who speaks with Reyes, in part 2 (The Body). I have tried to click equally on certain parts of the screen but nothing has happened. On the other hand, if it is Reyes, who talks to Ray, it works normally.

There is still a bug in the script, I had opened this issue: (#123), I think that now the two script bugs do not occur, but I have to do more tests about

from engge.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.