CREATE YOUR AI CUM SLUT ON CANDY.AI TRY FOR FREE
x

Madkingofmadness

New Member
Oct 2, 2017
8
35
I'm having trouble with the resolution, the window is the size of a playing card, and the settings just change from full screen to window, does anyone have a solution?
 

sanndan

Newbie
Jun 27, 2019
73
7
This VN is untranslatable because there's no available tool to create the patch for it
 

Rosen King

Engaged Member
May 29, 2019
2,257
1,742
This translation mostly happened because I wanted to test my tools/scripts and those games use another engine.
Saiminjutsu 1's remake (that uses this engine) has someone actually interested in properly translating it so I won't touch it. There is a partial for the prologue of saimin 2 and ura saimin 2 here https://f95zone.to/threads/translation-request-saiminjutsu-2-black-rainbow.90051/post-13627477.

The games are all standalone so no reason to play in any order tbh.
What about 4?
 

FrangoLight

New Member
Jul 30, 2024
4
0
I was able to reproduce this. Delete _FONTSET.MED before running with Locale Emulator, it should work then.
You don't have permission to view the spoiler content. Log in or register now.
It worked, thank you very much for the help! If you can do more translations of Mind Control/Saimin games, I'm a big fan of the genre. Once again, thank you for your patience and help.
 

ShdowNepto

Member
Nov 8, 2018
102
54
It worked, thank you very much for the help! If you can do more translations of Mind Control/Saimin games, I'm a big fan of the genre. Once again, thank you for your patience and help.
Second this, the Soushinjutsu games for Studio Jaren occupy a similar engine, it would be good if these classic novels were translated.
 

flwqo

Member
Modder
Jun 6, 2021
166
231
For posterity, here's the stand-alone translation. The reason to include the translation separately is so an improved translation can be used in the future if someone manually translates it or machine translations improve. This assumes you have the original version with Japanese of course.

Edit: virus scan is a false positive according to this.
 
Last edited:

Rosen King

Engaged Member
May 29, 2019
2,257
1,742
For posterity, here's the stand-alone translation. The reason to include the translation separately is so an improved translation can be used in the future if someone manually translates it or machine translations improve. This assumes you have the original version with Japanese of course.
Uh... Why does it straight up say "[VIRUS]"?
 

flwqo

Member
Modder
Jun 6, 2021
166
231
Uh... Why does it straight up say "[VIRUS]"?
Because of this:
The exe is the original game exe with a few bytes patched for the translation.
The dlls are from/for Locale Emulator and yes they do inject into the game but it is to make it work on non Japanese Windows.
Basically yes, it is a false positive.
False positive according to OP. It's just the same files they included in the release.
 

Rosen King

Engaged Member
May 29, 2019
2,257
1,742
Because of this:

False positive according to OP. It's just the same files they included in the release.
I'm not talking about a virus scan, though, the name of the file literally says "[VIRUS] Saimin3_ENG.7z". Like, that's straight up how it's written as an attachment under your post.
 
Last edited:

flwqo

Member
Modder
Jun 6, 2021
166
231
I'm not talking about a virus scan, though, the name of the file literally says "[VIRUS] Saimin3_ENG.7z". Like, that's straight up how it's written as an attachement under your post.
I know. It's automatically added by F95, probably after a virus scan.
 

flwqo

Member
Modder
Jun 6, 2021
166
231
xorxorrax what tools did you use to unpack, repack the .med files? The artwork is pretty low resolution, and the game upscaler isn't very good with noticeable pixelation. I want to see if I can improve/replace the pictures.
 

Rosen King

Engaged Member
May 29, 2019
2,257
1,742
So I've noticed two significant errors so far. During the first scene in Satsuki's "evil" path, there's a line where two kanji are left untranslated for some reason. ("For now, I need to:BootyTime::BootyTime: the suggestion.") And when choosing between Tsubasa's "good" and "evil" path, the latter is just an empty choice box.
 

xorxorrax

Member
Modder
Apr 12, 2020
313
1,603
xorxorrax what tools did you use to unpack, repack the .med files? The artwork is pretty low resolution, and the game upscaler isn't very good with noticeable pixelation. I want to see if I can improve/replace the pictures.
Use for better upscaling, the issue is that the game uses Nearest Neighbour scaling when Fullscreen and magpie allows you to pick better algorithms. Don't think it would be possible to upscale the pictures in the game as everything is hardcoded to 720p.

As for tools I used, I pretty much had to make my own and also , the only tool that existed beforehand was a string replacer and repacker. Dunno who the author is but from the comments probably Chinese.

If you really want to replace some images, here is the image encoder/decoder I made. Note that it does no compression so the file sizes are way bigger then the original:
Python:
from pathlib import Path
from dataclasses import dataclass
from struct import unpack, pack
from glob import glob

from PIL import Image

unpack_u16 = lambda x: unpack("<H", x)[0]
unpack_u32 = lambda x: unpack("<I", x)[0]

@dataclass
class YB:
    width: int
    height: int
    bpp: int
    flag: int
    packed_sz: int
    unpacked_sz: int
    data: bytearray

    length_table: tuple = tuple(i + 3 for i in range(0xfe)) + (0x400, 0x1000)

    def __post_init__(self):
        if self.bpp == 3:
            self.image_format = "BGR"
        if self.bpp == 4:
            self.image_format = "BGRA"

    @staticmethod
    def _copy_overlap(data, src, dst, count):
        if dst > src:
            while count > 0:
                preceding = min(dst - src, count)
                data[dst:dst + preceding] = data[src:src + preceding]
                count -= preceding
                dst += preceding
        else:
            data[dst:dst + count] = data[src:src + count]

    @staticmethod
    def _is_dummy_alpha(data):
        alpha = data[3]
        if(alpha == 0xff):
            return False
        for i in range(7, len(data), 4):
            if data[i] != alpha:
                return False
        return True

    def unpack(self):
        if (self.flag & 0x40) != 0:
            unpacked_sz = self.unpacked_sz
        else:
            unpacked_sz = self.width * self.height * self.bpp

        output = bytearray(unpacked_sz)
        remaining = self.packed_sz
        bit = 0
        ctl = 0
        dst = 0
        cursor = 0

        while remaining > 0 and dst < len(output):
            bit >>= 1
            if bit == 0:
                ctl = self.data[cursor]
                cursor += 1
                remaining -= 1
                bit = 0x80
           
            if remaining <= 0:
                break

            if (ctl & bit) == 0:
                output[dst] = self.data[cursor]
                dst += 1
                cursor += 1
                remaining -= 1
                continue

            b = self.data[cursor]
            cursor += 1
            remaining -= 1
            shift = 0
            length = 0

            if (b & 0x80) != 0:
                if remaining <= 0:
                    break
                shift = self.data[cursor]
                cursor += 1
                remaining -= 1
                shift |= (b & 0x3f) << 8
                if (b & 0x40) != 0:
                    if remaining <= 0:
                        break
                    offset = self.data[cursor]
                    cursor += 1
                    remaining -= 1
                    length = self.length_table[offset]
                else:
                    length = (shift & 0xf) + 3
                    shift >>= 4
            else:
                length = b >> 2
                b &= 3
                if b == 3:
                    length += 9
                    output[dst:dst + length] = self.data[cursor:cursor + length]
                    if cursor + length > len(self.data):
                        break
                    cursor += length
                    remaining -= length
                    dst += length
                    continue
                shift = length
                length = b + 2

            shift += 1
            if dst < shift:
                raise ValueError("Invalid offset value")

            length = min(length, len(output) - dst)
            YB._copy_overlap(output, dst - shift, dst, length)
            dst += length

        if self.flag & 0x80 != 0:
            for i in range(self.bpp, len(output)):
                output[i] = (output[i] + output[i - self.bpp]) & 0xff

        if self.bpp == 4 and YB._is_dummy_alpha(output):
            self.image_format = "BGR32"

        return output
   
    @staticmethod
    def _dumb_pack(input_data):
        """
        Dumb pack algorithm. This does no compression at all, just
        packs the input data in a way that the game can unpack it.
        Tried to reverse the pack algorithm but i burnt too many
        brain cells and gave up.
        """
        output = bytearray()
        pos = 0
        input_length = len(input_data)

        while pos < input_length:
            ctl_pos = len(output)
            output.append(0)
            control_byte = 0x00
            bit = 0x80

            for i in range(8):
                if pos >= input_length:
                    break

                output.append(input_data[pos])
                pos += 1

                bit >>= 1

            output[ctl_pos] = control_byte

        return bytes(output)
   
    @staticmethod
    def _lz77_pack(input_data):
        """
        Tried to guess the LZ77 type algorithm used in the game.
        TODO: FIXME Doesn't work.
        """
        output = bytearray()
        pos = 0
        input_length = len(input_data)

        WINDOW_SIZE = 0x1000
        MIN_MATCH_LENGTH = 3

        while pos < input_length:
            ctl_pos = len(output)
            output.append(0)
            control_byte = 0
            bit = 0x80

            for i in range(8):
                if pos >= input_length:
                    break

                match_length = 0
                match_offset = 0
                max_match_length = min(0x1000, input_length - pos)

                for offset in range(1, min(pos, WINDOW_SIZE) + 1):
                    candidate_length = 0
                    while candidate_length < max_match_length and input_data[pos - offset + candidate_length] == input_data[pos + candidate_length]:
                        candidate_length += 1

                    if candidate_length > match_length:
                        match_length = candidate_length
                        match_offset = offset

                    if match_length >= MIN_MATCH_LENGTH:
                        break

                if match_length >= MIN_MATCH_LENGTH:
                    control_byte |= bit

                    if match_length in YB.length_table:
                        length_index = YB.length_table.index(match_length)
                    else:
                        length_index = len(YB.length_table) - 1

                    output.append(0x80 | (match_offset >> 8) | 0x40)
                    output.append(match_offset & 0xFF)
                    output.append(length_index)

                    pos += match_length
                else:
                    output.append(input_data[pos])
                    pos += 1

                bit >>= 1

            output[ctl_pos] = control_byte

        return bytes(output)

    @staticmethod
    def from_image(image: Image):
        if image.mode == "RGB":
            image = image.convert("RGBA")
        if image.mode != "RGBA":
            raise ValueError("Image must be in RGBA mode.")

        width, height = image.size
        bpp = 4
        flag = 0x00
        R, G, B, A = image.split()
        image = Image.merge("RGBA", (B, G, R, A))
        packed = bytearray(YB._dumb_pack(image.tobytes()))
       
        packed_sz = len(packed)
        unpacked_sz = 0xE7BEADDE
        return YB(width, height, bpp, flag, packed_sz, unpacked_sz, packed)
   
    def to_bytes(self):
        return b"YB" + bytes([self.flag, self.bpp]) + \
            pack("<II", self.packed_sz, self.unpacked_sz) + \
            pack("<HH", self.width, self.height) + self.data

"""
img_path = Path("gra") / "SYS_CFG2.prs"
with open(img_path, "rb") as f:
    magic = f.read(2)
    assert magic == b"YB"
    flag = f.read(1)[0]
    bpp = f.read(1)[0]
    assert bpp in (0x3, 0x4)
    packed_sz = unpack_u32(f.read(4))
    unpacked_sz = unpack_u32(f.read(4))
    width = unpack_u16(f.read(2))
    height = unpack_u16(f.read(2))
    data = f.read(packed_sz)

yb_img = YB(width, height, bpp, flag, packed_sz, unpacked_sz, data)
img = Image.frombytes("RGBA", (width, height), yb_img.unpack())
R, G, B, A = img.split()
img = Image.merge("RGBA", (B, G, R, A))

reverse = YB.from_image(img)
with open("reverse.yb", "wb") as f:
    f.write(reverse.to_bytes())

img = Image.frombytes("RGBA", (reverse.width, reverse.height), reverse.unpack())
R, G, B, A = img.split()
img = Image.merge("RGBA", (B, G, R, A))
"""

for img_path in glob("gra/*.png"):
    path = Path(img_path)

    img = Image.open(path)
    output = YB.from_image(img)

    path = Path("gra_output") / (path.stem + "_0")

    with open(path, "wb") as f:
        f.write(output.to_bytes())

    img = Image.frombytes("RGBA", (output.width, output.height), output.unpack())
    R, G, B, A = img.split()
    img = Image.merge("RGBA", (B, G, R, A))

    # display(img)
You also need to edit the MED.py repacker (this is the old tool I mentioned, I also used it to repack the images)
Code:
diff MED.py MED_gra.py
485c485
<         header = b'MDE0'+to_bytes(entry_length, 1)+b'\x00'
---
>         header = b'MDE1'+to_bytes(entry_length, 1)+b'\x00'
501c501
<             _file_data = MED.encrypt(_file_data, file_index['key'].encode())
---
>             #_file_data = MED.encrypt(_file_data, file_index['key'].encode())
 
  • Like
Reactions: tbu4 and flwqo

xorxorrax

Member
Modder
Apr 12, 2020
313
1,603
So I've noticed two significant errors so far. During the first scene in Satsuki's "evil" path, there's a line where two kanji are left untranslated for some reason. ("For now, I need to:BootyTime::BootyTime: the suggestion.") And when choosing between Tsubasa's "good" and "evil" path, the latter is just an empty choice box.
Fixed the kanji. Send me a save or the text that precedes the choice.
 

Boarborn

Member
Apr 11, 2022
109
101
what's with the specific fetish: watching younger girls poo (humiliation) - milfs get a softcore scat and anal sex

happens in this too:

i suspect the same group of people, lol