Install the app
How to install the app on iOS

Follow along with the video below to see how to install our site as a web app on your home screen.

Note: This feature may not be available in some browsers.

PersonA ~Opera Za no Kaijin~ (R-18) Proyect Translation

Yui87

Member
Elite
上級乙女
Nov 5, 2013
133
166
Hi, I'm trying to translate the otome game for PC PersonA ~Opera Za no Kaijin +18) initially in Spanish and later if someone helps me in English (I'm Spanish and I don't know much English), I would like to see if you know of any tool to be able to extract the texts of this game, a girl who is helping me cannot find any tool and says that it seems quite complicated, we have searched in forums from years ago that a girl began to translate the game into English but we did not find an answer :
https://forums.fuwanovel.net/topic/474-need-help-with-patching-a-vn/#comment-2836

I hope you can give us some help, thank you very much and best regards.
 
Hello, I am also interested in translating this game into Spanish and Princess Britain, but I don't know anything about programming, how has it gone?
 
  • Like
Reactions: Yui87
A post in the linked thread contains the decompiled algorithm for the tool requested along with a hint for the decryption key.

https://forums.fuwanovel.net/topic/474-need-help-with-patching-a-vn/#comment-2892

Basically, take that C code and make it read input from a file and produce output. That will give you the tool you were requesting. Then just do the reverse to repackage arbitrary files so the game can read them. That means either learn programming yourself, or finding someone to write a tool based upon that code in that post.
 
The girl who was helping me managed to extract the texts, the problem was inserting them again; She couldn't insert the amount of text 😭.
There we stay and there is no progress. I have no idea about programming, so if someone can solve the problem of reinserting the texts it would be very helpful.
Anyway, thank you very much ☺️
A post in the linked thread contains the decompiled algorithm for the tool requested along with a hint for the decryption key.

https://forums.fuwanovel.net/topic/474-need-help-with-patching-a-vn/#comment-2892

Basically, take that C code and make it read input from a file and produce output. That will give you the tool you were requesting. Then just do the reverse to repackage arbitrary files so the game can read them. That means either learn programming yourself, or finding someone to write a tool based upon that code in that post.

.
 
Would you consider sharing the source code and/or the files (binary/script) that were used to extract the text?

Doing the same operation in reverse, where there is already code to handle the input/output file streams, is a lot simpler than having to work from only the raw function and would prove that the function works. A loose file that has an example of the expected raw output would also be very helpful.
 
  • Like
Reactions: Yui87
I give you what the girl found out with a friend of hers:

"Analyzed the engine and there are multiple problems:

1. The engine program that unifies everything and runs the game program.

The first thing we did was try to find out what engine it works with to see if there is some kind of tool that helps us extract the files like with many Visual Novels.
We found the name they gave the engine when examining the .Exe file, it is called "Game Script Executer" (GSE).

The problem is that there was no information about it, the only thing known about this is about the company Mirai.Soft, which was the developer of PersonA. We didn't find anything that would facilitate the files because it is not a mass-produced engine like the games made in Ren'py, Kirikiri...


2. With the Fuwanovel forum we were finally able to find that the girl shared part of the code to understand how to extract part of the text.

Here the nightmare begins and that's the bad part: we confirmed that the engine was programmed by a person from the company and she did it in such a way to ensure that piracy would be a nightmare.

We already understood what the translator was referring to, the game is tedious to patch because:

- You cannot change the amount of letters or spaces in the game, if you add text that requires a jump the game crashes.

Even so, we tried to change the text, but the engine directly has complications and if we say that the prologue has 7 lines, but you insert the text of the translated prologue and they make you 9, then 3 lines of the game will disappear.
And I don't mean that 3 lines will not be shown, it's that 3 lines from any part of the game will disappear from the program as if it were eliminating itself due to the lack of spaces.

We found a solution for this and it was what the girl was referring to: instead of inserting text you just overwrite it letter by letter, it's super tedious, but it was the only way we found to do "something". The worst thing is that we don't know how to repackage everything back and she wouldn't let us, but we already know what she meant by saying that she couldn't extract the text but had to directly change it letter by letter.

The engine is super cumbersome, the bad thing is that there is no information about it because it was completely created from scratch, otherwise it would make things easier".

Would you consider sharing the source code and/or the files (binary/script) that were used to extract the text?

Doing the same operation in reverse, where there is already code to handle the input/output file streams, is a lot simpler than having to work from only the raw function and would prove that the function works. A loose file that has an example of the expected raw output would also be very helpful.
 
Last edited:
  • Like
Reactions: pio
While I was sleeping, I figured out that if I map an instance of extracted text to a (psudo-)random number and insert that number/code point in the place of the text, then I can correlate the text to where it was extracted.

The rest of the in-game text can just be replaced with empty spaces so it does not show up at all after the actual JPN text has been extracted.

Then, those numbers/code points in the font used by the game can be replaced with blank spaces, or perhaps just find blank spaces in the font natively, so the random numbers/code points do not actually show up visibly.

Despite the characters not displaying in game, they are present and should be searchable programmatically using text extraction software directly by their identifiers (\u034), or however Shift-JIS or UTF-16 BOM LE stores stuff internally. Anyway, they should show up and be searchable in that database created earlier, which makes it possible to map an instance of where text came from to where it should go.

Thus, if both the original JPN text was available and also if a translation was created using such text, the resulting translation could be overlayed onto the game using overlay software that inserts arbitrary bits of text based upon that database.

It is not necessarily the ideal way to do things, but if the alternatives are decompiling the software or obtaining the game developer's tools, neither of which have worked so far, would you be willing to consider using overlay software to publish a translation for the game instead?

Which brings me back to my original question, would you be willing to share the tool used to extract text from this game, and preferably any other tools already developed for it? Even if that function posted earlier can somehow be used to do it, having a finished binary proves it is possible. The fuction in its current form does not do that.

Just upload the files related to this project to github or something if you want other people to further the work on this.
 
  • Love
Reactions: Yui87
While I was sleeping, I figured out that if I map an instance of extracted text to a (psudo-)random number and insert that number/code point in the place of the text, then I can correlate the text to where it was extracted.

The rest of the in-game text can just be replaced with empty spaces so it does not show up at all after the actual JPN text has been extracted.

Then, those numbers/code points in the font used by the game can be replaced with blank spaces, or perhaps just find blank spaces in the font natively, so the random numbers/code points do not actually show up visibly.

Despite the characters not displaying in game, they are present and should be searchable programmatically using text extraction software directly by their identifiers (\u034), or however Shift-JIS or UTF-16 BOM LE stores stuff internally. Anyway, they should show up and be searchable in that database created earlier, which makes it possible to map an instance of where text came from to where it should go.

Thus, if both the original JPN text was available and also if a translation was created using such text, the resulting translation could be overlayed onto the game using overlay software that inserts arbitrary bits of text based upon that database.

It is not necessarily the ideal way to do things, but if the alternatives are decompiling the software or obtaining the game developer's tools, neither of which have worked so far, would you be willing to consider using overlay software to publish a translation for the game instead?

Which brings me back to my original question, would you be willing to share the tool used to extract text from this game, and preferably any other tools already developed for it? Even if that function posted earlier can somehow be used to do it, having a finished binary proves it is possible. The fuction in its current form does not do that.

Just upload the files related to this project to github or something if you want other people to further the work on this.
Unfortunately we don't have any tools, we couldn't unzip the game 😭.
I'll try to look through all the blog entries of the girl she was translating the game to see if there are any clues as to how she did it:

https://personaphantomoftheopera.wordpress.com/about/

I hope there is someone who figures it out.
Anyway thank you very much.
 
  • Like
Reactions: Entai2965
Unfortunately we don't have any tools, we couldn't unzip the game 😭.
I'll try to look through all the blog entries of the girl she was translating the game to see if there are any clues as to how she did it:

https://personaphantomoftheopera.wordpress.com/about/

I hope there is someone who figures it out.
Anyway thank you very much.
In that case, maybe an offline version of MTool would be the best path forward. I do not see this happening otherwise.
 
I'm trying to try Annot Player to see if I could add subtitles to the game, but I'm a disaster and I don't understand anything, I've searched for videos on YouTube and I still don't understand how to do the translation and implement the subtitles:



If anyone can help me I would be very grateful.
 
I'm trying to try Annot Player to see if I could add subtitles to the game, but I'm a disaster and I don't understand anything, I've searched for videos on YouTube and I still don't understand how to do the translation and implement the subtitles:


it looks interesting, how is this different to a textractor? (which if it serves in this game) I saw it the same, it does not allow to create a patch as such, that's what I understood watching the videos
 
Last edited:
I am not working on this, yet. Just posting some information so we are all on the same page.

Here is the vndb link: https://vndb.org/v7585

Partial web-only translation is over here still https://personaphantomoftheopera.wordpress.com/intro/

Here is what Garbro shows for the main executable
Code:
BLOCK "040004b0"
{
    VALUE "CompanyName", "K.K. MADOKA"
    VALUE "FileDescription", "GSX"
    VALUE "FileVersion", "3, 0, 0, 0"
    VALUE "InternalName", "GSX"
    VALUE "LegalCopyright", "Copyright (C) 1999,2011 K.K. MADOKA"
    VALUE "OriginalFilename", "GSX.exe"
    VALUE "ProductName", "Game Script eXecuter"
    VALUE "ProductVersion", "3, 0, 0, 0"
}

Here is the Garbro source code for the K5 archives https://github.com/crskycode/GARbro/blob/master/Legacy/Gsx/ArcK5.cs

The K5 archive code does not work to unpack the scenario archive/file. Some of the archives start with a "CRPT" header, including the .sce (scenario?) and .gsdb (GSX database?) files.

VNDB has a list of games which use this engine https://vndb.org/r?f=foGSX Which leads to https://vndb.org/v7218 which VNDB lists as using the same game engine called
笑わないエリカ
Warawanai Erika
Erika which isn't laughed at

That game has an AI translation patch (Claude-Opus 4.5 Jpn->Chinese simplified) over here. https://www.moyu.moe/resource/8703
Extraction password in 7-zip is `b站:错误解释`.
It only contains the translated .sce and the changed font and an empty text file that says to use the font, probably using some chinese software that overrides the font.

I also dumped the decompiled assembler code from https://forums.fuwanovel.moe/topic/474-need-help-with-patching-a-vn/#findComment-2892 into an AI and it gave me this.

AI output on the algorithm:
Q: This function decrypts something. What does it do and how does it work in very general terms?
Code:
signed int __thiscall sub_44D46D(int this)
{
  ...
  bytesRead = sub_44D035(*(HANDLE *)(v1 + 1044), Buffer, 0x400u, 0);
  memset(&Buffer[bytesRead], 0, 1024 - bytesRead);
  v3 = v1 + 1209;
  if ( *(_DWORD *)(v1 + 1156) == 196609 )
  {
    *(_BYTE *)(v1 + 1208) = v15;
    v4 = 67;
    v13 = 1023;
    do
    {
      v5 = *(_BYTE *)(v3 - 1) ^ Buffer[v4];
      v4 = (v4 + 23) & 0x3FF;
      *(_BYTE *)v3++ = v5;
      --v13;
    }
    while ( v13 );
    v6 = v1 + 1208;
    v7 = 256;
    do
    {
      *(_DWORD *)v6 ^= *(_DWORD *)((char *)&unk_4C4098 + v6 - v1 - 1208); // 0x4C4098 probably contains the encryption key
      v6 += 4;
      --v7;
    }
    while ( v7 );
    memset((void *)(v1 + bytesRead + 1208), 0, 1024 - bytesRead);
  }
...
}

A: In very general terms, this function performs a two-stage decryption on a chunk of data read from a file or stream. It uses a combination of a rolling XOR cipher and a block-based XOR cipher.

Here is the breakdown of how it works:

1. Data Loading
The function starts by reading up to 1024 bytes of raw data from a file handle into a local Buffer. It then clears any remaining space in that buffer to ensure no "garbage" data interferes with the process.

2. Stage One: The Rolling XOR (Stream Cipher)
The first do-while loop performs a byte-by-byte transformation.
Mechanism: It takes a byte from the buffer and XORs it against a byte from a specific memory location (v3 - 1).
The "Rolling" Aspect: Instead of moving through the buffer linearly (0, 1, 2, 3...), it uses a custom step pattern: v4 = (v4 + 23) & 0x3FF. This means it jumps through the buffer in steps of 23.
Purpose: This is a common technique used to obfuscate data. By jumping around the buffer rather than reading it straight through, it prevents simple pattern recognition and makes the decryption harder to reverse-engineer without knowing the exact jump increment (23) and the starting offset (67).

3. Stage Two: The Block XOR (Keyed Decryption)
The second do-while loop performs a much faster, bulk decryption.
Mechanism: It processes the data 4 bytes at a time (*(_DWORD *)) rather than 1 byte at a time. This is an optimization to speed up the decryption.
The Key: It XORs the data against a fixed key stored in the program's data section (at address 0x4C4098).
Purpose: This is the "heavy lifting" part of the decryption. Once the initial obfuscation (Stage One) is stripped away, this stage applies the actual cryptographic key to turn the scrambled bytes back into usable data.

Summary of the Logic Flow
Read raw encrypted data from a file.
De-shuffle the data using a non-linear (jumping) XOR pattern.
Decrypt the de-shuffled data using a 4-byte block XOR against a hardcoded global key.
Store the final, plain-text result in a memory structure starting at offset 1208.

Analogy:
Imagine you have a sentence where the letters are written in a strange, skipping order (Stage 1), and then every letter has been replaced by a different letter using a secret codebook (Stage 2). To read it, you first have to re-order the letters into the right sequence, and then apply the codebook to reveal the actual words.

Python version (by AI, unclear if works or not)

To write this in Python, we need to simulate how C handles memory (specifically pointers and fixed-size buffers). Since Python doesn't use raw memory addresses, we will use `bytearray` to represent the buffers and a list/bytes object to represent the "Global Key" found at `0x4C4098`.

Here is the functional equivalent in Python:

Code:
def decrypt_function(input_buffer, global_key, magic_value):
    """
    Python implementation of the provided C decryption logic.

    :param input_buffer: bytes/bytearray containing the data read from file
    :param global_key: bytes/bytearray containing the key from 0x4C4098
    :param magic_value: The value at v1 + 1156 (used for the 'if' check)
    :return: The decrypted bytearray
    """

    # Constants from the C code
    BUFFER_SIZE = 1024
    MAGIC_CONSTANT = 196609

    # Ensure input buffer is treated as a 1024 byte array (simulating memset 0)
    work_buffer = bytearray(input_buffer[:BUFFER_SIZE])
    if len(work_buffer) < BUFFER_SIZE:
        work_buffer.extend(bytearray(BUFFER_SIZE - len(work_buffer)))

    # The 'if' check in the original code
    if magic_value != MAGIC_CONSTANT:
        return work_buffer # Return original if magic value doesn't match

    # This represents the 'v3' memory location where decrypted data is stored
    # In C, v3 points to v1 + 1209. We will simulate the decrypted output separately.
    decrypted_output = bytearray(BUFFER_SIZE)

    # --- STAGE 1: Rolling XOR ---
    # v4 = 67, v13 = 1023, v3 = v1 + 1209
    # The C code uses *(_BYTE *)(v3 - 1) ^ Buffer[v4]
    # This implies the XOR uses a value stored just before the output buffer.
    # For this simulation, we'll assume the 'v3-1' value is a seed/constant.
    seed_byte = 0xAA # Placeholder for the value at *(v3-1)

    v4 = 67
    v13 = 1023

    # We simulate the 'do-while' loop
    # Note: The C code increments v3, effectively filling the output buffer
    for _ in range(v13):
        # v5 = *(_BYTE *)(v3 - 1) ^ Buffer[v4]
        v5 = (seed_byte ^ work_buffer[v4]) & 0xFF

        # v4 = (v4 + 23) & 0x3FF
        v4 = (v4 + 23) & 0x3FF

        # In C: *v3++ = v5. We store this in our output buffer.
        # We use a simple index tracker to simulate the v3 pointer increment
        # Since the loop runs 1023 times, we fill index 0 to 1022
        decrypted_output[_] = v5 

    # --- STAGE 2: Block XOR (4-byte chunks) ---
    # v6 = v1 + 1208. In C, this is the start of the decrypted data.
    # It XORs the data with the global_key using 4-byte (DWORD) chunks.

    # We process 256 iterations (v7 = 256)
    # Each iteration handles 4 bytes (v6 += 4)
    for i in range(0, 256 * 4, 4):
        if i + 4 > BUFFER_SIZE or i + 4 > len(global_key):
            break

        # Simulate: *(_DWORD *)v6 ^= *(_DWORD *)key
        # We convert 4 bytes to an integer, XOR them, and convert back

        # Get 4 bytes from decrypted_output
        chunk = int.from_bytes(decrypted_output[i:i+4], byteorder='little')

        # Get 4 bytes from global_key
        key_chunk = int.from_bytes(global_key[i:i+4], byteorder='little')

        # XOR
        decrypted_chunk = chunk ^ key_chunk

        # Write back to decrypted_output
        decrypted_output[i:i+4] = decrypted_chunk.to_bytes(4, byteorder='little')

    return decrypted_output

# --- EXAMPLE USAGE ---
if __name__ == "__main__":
    # Mock data
    mock_file_data = bytearray([i % 256 for i in range(1024)])
    mock_key = bytearray([0x12, 0x34, 0x56, 0x78] * 256) # 1024 bytes of key
    mock_magic = 196609

    result = decrypt_function(mock_file_data, mock_key, mock_magic)
    print(f"First 16 bytes of decrypted data: {result[:16].hex(' ')}")

### Key Implementation Details:

1. **The Rolling XOR (`v4 = (v4 + 23) & 0x3FF`)**:
I used a standard `for` loop to simulate the `do-while`. The `& 0x3FF` is a bitwise mask that ensures the index stays within a range of 1024 (0-1023), which is exactly what the original C code does to prevent buffer overflows.
2. **The Block XOR (`DWORD`)**:
In C, a `DWORD` is a 32-bit (4-byte) integer. To replicate `*(_DWORD *)v6 ^= ...`, I used Python's `int.from_bytes` with `byteorder='little'`. This is crucial because most x86 systems (where `__thiscall` is used) are **Little Endian**.
3. **The Pointer Math**:
C uses pointer arithmetic (like `v3++` or `v6 += 4`). In Python, I replaced these with index-based slicing (`decrypted_output[i:i+4]`), which is the idiomatic and safe way to handle chunks of data.
4. **Memory Safety**:
The `memset` calls in the original C code are used to clear memory. In Python, we achieve this by initializing `bytearray(BUFFER_SIZE)`, which automatically fills the array with zeros.

LunaTranslator is probably the best way to translate this for now.
 
  • Like
Reactions: Aiceberry