Restore native inference, card-bound readings and paged dialogue. Provide manual, checksum-verified model setup instead of Git LFS so source checkouts work without uploading multi-gigabyte weights. The game stays offline; complete release archives still bundle the model.
174 lines
7.4 KiB
Python
174 lines
7.4 KiB
Python
"""Check that the shipping MASM tarot submission actually requests inference.
|
|
|
|
Builds a test-only entry point; never edits production sources or binaries.
|
|
Does not require model weights: even a missing-model error must be observable.
|
|
"""
|
|
from pathlib import Path
|
|
import struct
|
|
import subprocess
|
|
import argparse
|
|
|
|
from check_hall_expansion import ROOT, OUT, prepare
|
|
from check_artifacts import toolchain
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument('--with-model', action='store_true')
|
|
args = parser.parse_args()
|
|
prepare()
|
|
source = (OUT / "module.asm").read_text(encoding="cp1251")
|
|
entry = r"""
|
|
.data
|
|
fortuneAiProbePath db "build\hall-expansion\fortune-ai-probe.bin",0
|
|
fortuneAiProbeCount dd 0
|
|
fortuneAiProbeAnswerPath db "build\hall-expansion\fortune-ai-native-answer.bin",0
|
|
fortuneAiProbeStage dd 0
|
|
fortuneAiProbePagesPath db "build\hall-expansion\fortune-ai-pages.bin",0
|
|
fortuneAiProbePages dd 0
|
|
fortuneAiProbeShot db "build\hall-expansion\fortune-ai-stage-"
|
|
fortuneAiProbeDigit db "0.bmp",0
|
|
fortuneAiProbeQuestion db QUESTION_BYTES,0
|
|
.code
|
|
FortuneAiProbeStart PROC
|
|
call BeginFortuneDialog3D
|
|
invoke WindowEvents3D,0,102h,'1',0
|
|
lea esi,fortuneAiProbeQuestion
|
|
fortune_ai_probe_input:
|
|
movzx eax,BYTE PTR [esi]
|
|
test eax,eax
|
|
jz fortune_ai_probe_submit
|
|
invoke WindowEvents3D,0,102h,eax,0
|
|
inc esi
|
|
jmp fortune_ai_probe_input
|
|
fortune_ai_probe_submit:
|
|
call SubmitFortuneQuestion3D
|
|
; Pin the actual transmitted draw, before the ready frame is polled.
|
|
mov fortuneDrawCards3D,0
|
|
mov fortuneDrawCards3D[4],23
|
|
mov fortuneDrawCards3D[8],17
|
|
invoke CreateFileA,OFFSET fortuneAiProbePath,40000000h,0,0,2,80h,0
|
|
mov ebx,eax
|
|
invoke WriteFile,ebx,OFFSET fortuneWorkerState3D,4,OFFSET fortuneAiProbeCount,0
|
|
invoke WriteFile,ebx,OFFSET fortuneProcess3D,16,OFFSET fortuneAiProbeCount,0
|
|
invoke WriteFile,ebx,OFFSET fortuneRead3D,4,OFFSET fortuneAiProbeCount,0
|
|
invoke WriteFile,ebx,OFFSET fortuneWrite3D,4,OFFSET fortuneAiProbeCount,0
|
|
invoke FortuneCardSpeech3D,0
|
|
invoke WriteFile,ebx,OFFSET fortuneSpeech3D,256,OFFSET fortuneAiProbeCount,0
|
|
invoke CloseHandle,ebx
|
|
; MODEL_PROBE
|
|
call StopFortuneWorker3D
|
|
invoke ExitProcess,0
|
|
FortuneAiProbeStart ENDP
|
|
end FortuneAiProbeStart
|
|
"""
|
|
question = 'Стоит ли мне начать учиться рисованию?'
|
|
entry = entry.replace('QUESTION_BYTES', ','.join(map(str, question.encode('cp1251'))))
|
|
if args.with_model:
|
|
entry = entry.replace('; MODEL_PROBE', r'''
|
|
mov esi,12000
|
|
fortune_ai_probe_wait:
|
|
call PollFortuneWorker3D
|
|
cmp fortuneWorkerState3D,5
|
|
je fortune_ai_probe_ready
|
|
cmp fortuneWorkerState3D,4
|
|
je fortune_ai_probe_fail
|
|
invoke Sleep,15
|
|
dec esi
|
|
jnz fortune_ai_probe_wait
|
|
fortune_ai_probe_fail:
|
|
call StopFortuneWorker3D
|
|
invoke ExitProcess,88
|
|
fortune_ai_probe_ready:
|
|
invoke CreateFileA,OFFSET fortuneAiProbeAnswerPath,40000000h,0,0,2,80h,0
|
|
mov ebx,eax
|
|
invoke WriteFile,ebx,OFFSET fortuneAnswer3D,fortuneAnswerLength3D,OFFSET fortuneAiProbeCount,0
|
|
invoke CloseHandle,ebx
|
|
invoke CreateFileA,OFFSET fortuneAiProbePagesPath,40000000h,0,0,2,80h,0
|
|
mov fortuneAiProbePages,eax
|
|
; Run the real scene, including each page and explicit Continue input.
|
|
fortune_ai_probe_scene:
|
|
cmp fortunePhase3D,12
|
|
je fortune_ai_probe_done
|
|
cmp fortuneReadingShown3D,1
|
|
jne fortune_ai_probe_tick
|
|
mov fortuneSpeechTime3D,20000
|
|
call RenderFortuneDialog3D
|
|
cmp fortuneSpeechHeight3D,140
|
|
ja fortune_ai_probe_fail
|
|
cmp fortuneSpeechWidth3D,204
|
|
ja fortune_ai_probe_fail
|
|
mov eax,fortuneSection3D
|
|
mov fortuneAiProbeStage,eax
|
|
add al,'0'
|
|
mov fortuneAiProbeDigit,al
|
|
invoke ExpansionSnapshot,OFFSET fortuneAiProbeShot
|
|
invoke WriteFile,fortuneAiProbePages,OFFSET fortuneSection3D,4,OFFSET fortuneAiProbeCount,0
|
|
invoke WriteFile,fortuneAiProbePages,OFFSET fortuneSpeechLength3D,4,OFFSET fortuneAiProbeCount,0
|
|
invoke WriteFile,fortuneAiProbePages,OFFSET fortuneSpeech3D,fortuneSpeechLength3D,OFFSET fortuneAiProbeCount,0
|
|
mov mouseX3D,20
|
|
mov mouseY3D,232
|
|
mov mouseClick3D,1
|
|
call HandleFortuneDialog3D
|
|
fortune_ai_probe_tick:
|
|
add animationClock3D,100
|
|
call UpdateFortuneSession3D
|
|
jmp fortune_ai_probe_scene
|
|
fortune_ai_probe_done:
|
|
invoke CloseHandle,fortuneAiProbePages
|
|
cmp fortuneRevealed3D,3
|
|
jne fortune_ai_probe_fail
|
|
cmp fortuneAiProbeStage,3
|
|
jne fortune_ai_probe_fail
|
|
''')
|
|
assert source.count("end ExpansionTestStart") == 1
|
|
source = source.replace("end ExpansionTestStart", 'include tools/tests/fortune_ai_test.inc\n' + entry)
|
|
source = source.replace(' invoke ExitProcess,0\nFortuneAiProbeStart ENDP',
|
|
' call FortuneAiNativeTests\n invoke ExitProcess,0\nFortuneAiProbeStart ENDP')
|
|
stem = "fortune-ai-probe"
|
|
(OUT / (stem + ".asm")).write_text(source, encoding="cp1251")
|
|
assembler, linker = toolchain()
|
|
subprocess.run([str(assembler), "/c", "/coff", "/Cp", "/nologo", "/I.",
|
|
f"/Fobuild/hall-expansion/{stem}.obj",
|
|
f"build/hall-expansion/{stem}.asm"], cwd=ROOT, check=True)
|
|
sdk = sorted(Path("C:/Program Files (x86)/Windows Kits/10/Lib").glob("*/um/x86"))[-1]
|
|
subprocess.run([str(linker), "/nologo", "/SUBSYSTEM:CONSOLE",
|
|
"/ENTRY:FortuneAiProbeStart@0", f"/LIBPATH:{sdk}",
|
|
f"/OUT:build/hall-expansion/{stem}.exe",
|
|
f"build/hall-expansion/{stem}.obj", "build/hall-expansion/helpers.obj",
|
|
"kernel32.lib", "user32.lib", "gdi32.lib", "winmm.lib", "ws2_32.lib"],
|
|
cwd=ROOT, check=True)
|
|
subprocess.run([str(OUT / (stem + ".exe"))], cwd=ROOT, timeout=210 if args.with_model else 30, check=True)
|
|
raw = (OUT / "fortune-ai-probe.bin").read_bytes()
|
|
state, process, thread, pid, tid, reader, writer = struct.unpack_from("<7I", raw)
|
|
speech = raw[28:].split(b"\0", 1)[0].decode("cp1251")
|
|
print(f"Native submit: state={state}, process={process}, reader={reader}, writer={writer}")
|
|
print("Card speech:", ascii(speech))
|
|
assert (state == 1 and process and reader and writer) or (state == 4 and not process), (
|
|
"Tarot submission neither starts/queues inference nor reports an AI error; "
|
|
"the actual native scene cannot produce a model reading."
|
|
)
|
|
if args.with_model:
|
|
from check_fortune_ai import check_reading
|
|
text = (OUT / 'fortune-ai-native-answer.bin').read_bytes().decode('cp1251').replace('\0', '\n')
|
|
check_reading(text, [0, 23, 17])
|
|
pages = (OUT / 'fortune-ai-pages.bin').read_bytes()
|
|
recovered = [[] for _ in range(4)]
|
|
offset = 0
|
|
while offset < len(pages):
|
|
section, size = struct.unpack_from('<II', pages, offset)
|
|
offset += 8
|
|
page = pages[offset:offset + size].decode('cp1251')
|
|
offset += size
|
|
assert section < 4 and size <= 255, (section, size)
|
|
assert len(page.splitlines()) <= 9, page
|
|
assert all(len(line) <= 23 for line in page.splitlines()), page
|
|
recovered[section].append(page)
|
|
for expected, actual in zip(text.splitlines(), recovered):
|
|
assert ' '.join(expected.split()) == ' '.join(' '.join(actual).split()), (expected, actual)
|
|
print('PASS: actual native WM_CHAR -> worker -> model -> four paged scene stages')
|
|
print("PASS: native submit, partial IPC frames, bounded parser/paging, timeout and cleanup")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|