fix: stabilize gameplay and IDE source writes
Trace shots against current map geometry instead of stale render depth. Preserve outline counters across volatile pixel calls and freeze gameplay on cinematic, transition and death ticks. Clamp packed HP before narrowing and match legacy archetype rules. Publish IDE saves atomically; failed saves block file switches and builds. Drain compiler output while hidden. Verify runtime/editor builds, 112 editor checks, 148 ASM checks and live preview parity. Record remaining stages in docs/STABILIZATION.md.
This commit is contained in:
parent
c1da810bd5
commit
f42679fe62
18 changed files with 594 additions and 70 deletions
67
docs/STABILIZATION.md
Normal file
67
docs/STABILIZATION.md
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
# Stabilization roadmap
|
||||
|
||||
Stages are delivered separately. Each completed stage is tested, committed and
|
||||
pushed before work stops for confirmation. Editor and runtime UI redesign will
|
||||
use an Unreal Engine 5-inspired visual style; stage 1 does not redesign UI.
|
||||
|
||||
## Stage 1 — correctness and data safety
|
||||
|
||||
Implemented:
|
||||
|
||||
- `DrawRectOutline` keeps loop counters in a callee-saved register, not the
|
||||
volatile registers overwritten by `DrawPixel`. Nonpositive sizes draw nothing.
|
||||
- `TraceShotWall` traverses current map cells independently of rendering. Distances
|
||||
are 24.8 world units; closed doors, glass, portals and out-of-map cells block
|
||||
shots. Open doors do not. Corner-adjacent walls block diagonal seam shots.
|
||||
- Hitscan chooses the nearest live guard intersecting the forward billboard
|
||||
hitbox. Half-width is 16 world units times authored scale (zero scale means
|
||||
legacy 1.0), not a constant number of screen pixels.
|
||||
- `IsGameplayBlocked` centralizes gameplay gating. `UpdatePlayer` latches blocked
|
||||
ticks before processing cinematic state. AI, pickups and weapon timers freeze
|
||||
during active/pending cutscenes and room fades. END/skip and instant transitions
|
||||
cannot resume AI or fire stale input halfway through a tick. Death blocks player
|
||||
movement, turning and firing; lethal damage stops the remaining entity pass.
|
||||
- Packed entity metadata remains binary-compatible. Only `flags == 0` enables
|
||||
legacy texture-based archetypes; any nonzero flags explicitly encode the low
|
||||
type byte, including Prop. Health uses one byte: 1..255, zero = legacy 100 HP.
|
||||
The C++ setter clamps before narrowing, and the inspector exposes 1..255.
|
||||
- IDE source writes use a unique sibling temporary file, checked full write,
|
||||
flush, close and atomic replacement. Failure preserves disk contents and dirty
|
||||
buffer. Loading publishes a new document only after a complete valid text read.
|
||||
File switches and builds stop on failed autosave. Saves target the file actually
|
||||
opened, not a newly resolved fallback path.
|
||||
- Compiler output is drained even with the IDE hidden. Process startup uses the
|
||||
system command processor with quoting for project paths containing spaces.
|
||||
|
||||
### Verification
|
||||
|
||||
Windows commands:
|
||||
|
||||
```cmd
|
||||
build.bat
|
||||
editor\build_editor.bat
|
||||
python tools\test_regressions.py
|
||||
```
|
||||
|
||||
Results: editor **112/112**, ASM **148/148**. The suite also checks live x86/x64
|
||||
preview IPC, DX11 pixel equality, cinematic trace equality and unchanged project
|
||||
assets. New tests exercise production gameplay and IDE procedures, including
|
||||
failed atomic replacement under a Windows sharing lock and hidden compiler output
|
||||
exceeding pipe capacity. Interactive UI playtesting remains separate.
|
||||
|
||||
## Remaining stages
|
||||
|
||||
2. Predictable simulation: fixed 60 Hz ticks, fractional movement, normalized
|
||||
input, complete collision shape, current-room restart/reset policy. Extend
|
||||
gameplay gating into explicit states and separate cinematic camera ownership.
|
||||
3. Honest editor/gameplay: complete undo, keys/switches and supported interactions;
|
||||
UE5-inspired editor/runtime UI redesign.
|
||||
4. Project lifecycle and validated, safe standalone export.
|
||||
5. Measurements and targeted renderer/editor optimizations.
|
||||
6. Finished multi-room demo game and release verification.
|
||||
7. Data-driven extensions and subsequent rendering features.
|
||||
|
||||
Stage 1 intentionally does not claim that all audit findings are fixed: restart
|
||||
semantics, fixed timing, collision/AI visibility, full undo and exporter safety
|
||||
remain assigned to subsequent stages. Existing map and texture assets are not
|
||||
migrated or rewritten.
|
||||
Binary file not shown.
|
|
@ -2102,18 +2102,18 @@ void EditorApp::RenderInspector() {
|
|||
|
||||
if (ImGui::Combo("Тип сущности", &archType, s_ArchetypeNames, 6)) {
|
||||
PushUndo();
|
||||
SetSpriteEntityData(spr, static_cast<uint8_t>(archType), static_cast<uint8_t>(hp), static_cast<uint8_t>(spd), static_cast<uint8_t>(agg));
|
||||
SetSpriteEntityData(spr, static_cast<uint8_t>(archType), hp, static_cast<uint8_t>(spd), static_cast<uint8_t>(agg));
|
||||
}
|
||||
|
||||
if (archType == ENTITY_ARCHETYPE_GUARD) {
|
||||
if (ImGui::SliderInt("Здоровье (HP)", &hp, 10, 500)) {
|
||||
SetSpriteEntityData(spr, static_cast<uint8_t>(archType), static_cast<uint8_t>(hp), static_cast<uint8_t>(spd), static_cast<uint8_t>(agg));
|
||||
if (ImGui::SliderInt("Здоровье (HP)", &hp, 1, 255)) {
|
||||
SetSpriteEntityData(spr, static_cast<uint8_t>(archType), hp, static_cast<uint8_t>(spd), static_cast<uint8_t>(agg));
|
||||
}
|
||||
if (ImGui::SliderInt("Скорость", &spd, 1, 8)) {
|
||||
SetSpriteEntityData(spr, static_cast<uint8_t>(archType), static_cast<uint8_t>(hp), static_cast<uint8_t>(spd), static_cast<uint8_t>(agg));
|
||||
SetSpriteEntityData(spr, static_cast<uint8_t>(archType), hp, static_cast<uint8_t>(spd), static_cast<uint8_t>(agg));
|
||||
}
|
||||
if (ImGui::SliderInt("Радиус агрессии (ячейки)", &agg, 1, 15)) {
|
||||
SetSpriteEntityData(spr, static_cast<uint8_t>(archType), static_cast<uint8_t>(hp), static_cast<uint8_t>(spd), static_cast<uint8_t>(agg));
|
||||
SetSpriteEntityData(spr, static_cast<uint8_t>(archType), hp, static_cast<uint8_t>(spd), static_cast<uint8_t>(agg));
|
||||
}
|
||||
} else if (archType == ENTITY_ARCHETYPE_MEDKIT) {
|
||||
ImGui::TextColored(ImVec4(0.2f, 1.0f, 0.4f, 1.0f), "Восстанавливает +25 HP игроку при подборе");
|
||||
|
|
|
|||
|
|
@ -93,6 +93,7 @@ static const int g_ScriptTemplateCount = sizeof(g_ScriptTemplates) / sizeof(g_Sc
|
|||
struct CodeEditorState {
|
||||
int currentFile = 0;
|
||||
std::string fileContent;
|
||||
fs::path loadedPath;
|
||||
std::vector<char> textBuffer;
|
||||
bool isModified = false;
|
||||
std::string statusText = "Готов к редактированию кода MASM32";
|
||||
|
|
@ -120,50 +121,64 @@ struct CodeEditorState {
|
|||
|
||||
void LoadFile(int fileIndex, const std::string& projectRoot) {
|
||||
if (fileIndex < 0 || fileIndex >= g_CodeFileCount) return;
|
||||
currentFile = fileIndex;
|
||||
if (isModified && !SaveFile(projectRoot)) return;
|
||||
fs::path fullPath = fs::path(projectRoot) / g_ProjectCodeFiles[fileIndex].relativePath;
|
||||
if (!fs::exists(fullPath)) {
|
||||
fullPath = fs::path(projectRoot).parent_path() / g_ProjectCodeFiles[fileIndex].relativePath;
|
||||
}
|
||||
|
||||
HANDLE hFile = CreateFileA(fullPath.string().c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||
if (hFile != INVALID_HANDLE_VALUE) {
|
||||
DWORD fileSize = GetFileSize(hFile, nullptr);
|
||||
if (fileSize != INVALID_FILE_SIZE) {
|
||||
EnsureBufferSize(fileSize);
|
||||
DWORD read = 0;
|
||||
ReadFile(hFile, textBuffer.data(), fileSize, &read, nullptr);
|
||||
textBuffer[read] = '\0';
|
||||
fileContent = std::string(textBuffer.data(), read);
|
||||
isModified = false;
|
||||
statusText = "Открыт файл: " + g_ProjectCodeFiles[fileIndex].displayName;
|
||||
}
|
||||
CloseHandle(hFile);
|
||||
} else {
|
||||
fileContent.clear();
|
||||
EnsureBufferSize(1024);
|
||||
textBuffer[0] = '\0';
|
||||
isModified = false;
|
||||
if (hFile == INVALID_HANDLE_VALUE) {
|
||||
statusText = "Не удалось открыть файл: " + fullPath.string();
|
||||
return;
|
||||
}
|
||||
LARGE_INTEGER size{};
|
||||
bool ok = GetFileSizeEx(hFile, &size) && size.QuadPart >= 0 && size.QuadPart <= 16 * 1024 * 1024;
|
||||
std::vector<char> incoming(ok ? static_cast<size_t>(size.QuadPart) + 1 : 1, 0);
|
||||
DWORD read = 0;
|
||||
if (ok) ok = ReadFile(hFile, incoming.data(), static_cast<DWORD>(size.QuadPart), &read, nullptr) &&
|
||||
read == static_cast<DWORD>(size.QuadPart);
|
||||
CloseHandle(hFile);
|
||||
if (!ok || std::memchr(incoming.data(), '\0', read)) {
|
||||
statusText = "Ошибка чтения исходного файла: " + fullPath.string();
|
||||
return;
|
||||
}
|
||||
// Publish a document only after a complete read. Failed loads keep the old buffer.
|
||||
currentFile = fileIndex;
|
||||
loadedPath = fs::absolute(fullPath);
|
||||
fileContent.assign(incoming.data(), read);
|
||||
textBuffer = std::move(incoming);
|
||||
EnsureBufferSize(read);
|
||||
isModified = false;
|
||||
statusText = "Открыт файл: " + g_ProjectCodeFiles[fileIndex].displayName;
|
||||
}
|
||||
|
||||
bool SaveFile(const std::string& projectRoot) {
|
||||
if (currentFile < 0 || currentFile >= g_CodeFileCount) return false;
|
||||
fs::path fullPath = fs::path(projectRoot) / g_ProjectCodeFiles[currentFile].relativePath;
|
||||
if (!fs::exists(fullPath)) {
|
||||
fullPath = fs::path(projectRoot).parent_path() / g_ProjectCodeFiles[currentFile].relativePath;
|
||||
if (currentFile < 0 || currentFile >= g_CodeFileCount || loadedPath.empty() || textBuffer.empty()) {
|
||||
statusText = "Сначала откройте исходный файл.";
|
||||
return false;
|
||||
}
|
||||
|
||||
HANDLE hFile = CreateFileA(fullPath.string().c_str(), GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||
const auto fullPath = loadedPath;
|
||||
static unsigned serial = 0;
|
||||
const auto temporary = fullPath.string() + ".tmp." + std::to_string(GetCurrentProcessId()) + "." + std::to_string(++serial);
|
||||
HANDLE hFile = CreateFileA(temporary.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||
if (hFile == INVALID_HANDLE_VALUE) {
|
||||
statusText = "Ошибка сохранения: доступ запрещен (" + fullPath.string() + ")";
|
||||
return false;
|
||||
}
|
||||
DWORD len = static_cast<DWORD>(std::strlen(textBuffer.data()));
|
||||
DWORD written = 0;
|
||||
WriteFile(hFile, textBuffer.data(), len, &written, nullptr);
|
||||
CloseHandle(hFile);
|
||||
bool ok = WriteFile(hFile, textBuffer.data(), len, &written, nullptr) && written == len;
|
||||
if (ok) ok = FlushFileBuffers(hFile) != FALSE;
|
||||
if (!CloseHandle(hFile)) ok = false;
|
||||
if (ok) ok = MoveFileExA(temporary.c_str(), fullPath.string().c_str(),
|
||||
MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH) != FALSE;
|
||||
if (!ok) {
|
||||
DeleteFileA(temporary.c_str());
|
||||
statusText = "Ошибка сохранения; исходный файл сохранен: " + fullPath.string();
|
||||
return false;
|
||||
}
|
||||
fileContent.assign(textBuffer.data(), len);
|
||||
isModified = false;
|
||||
statusText = "Файл сохранен: " + g_ProjectCodeFiles[currentFile].displayName;
|
||||
return true;
|
||||
|
|
@ -173,7 +188,7 @@ struct CodeEditorState {
|
|||
if (isBuilding) return;
|
||||
|
||||
// Auto-save current file before building
|
||||
SaveFile(projectRoot);
|
||||
if (!SaveFile(projectRoot)) return;
|
||||
|
||||
buildLog.clear();
|
||||
buildErrors.clear();
|
||||
|
|
@ -214,9 +229,12 @@ struct CodeEditorState {
|
|||
si.wShowWindow = SW_HIDE;
|
||||
|
||||
PROCESS_INFORMATION pi{};
|
||||
std::string cmd = "cmd.exe /c \"" + buildBat.string() + "\"";
|
||||
char commandProcessor[MAX_PATH]{};
|
||||
GetSystemDirectoryA(commandProcessor, MAX_PATH);
|
||||
const auto cmdPath = fs::path(commandProcessor) / "cmd.exe";
|
||||
std::string cmd = "\"" + cmdPath.string() + "\" /d /s /c \"\"" + buildBat.string() + "\"\"";
|
||||
|
||||
if (CreateProcessA(nullptr, cmd.data(), nullptr, nullptr, TRUE, CREATE_NO_WINDOW, nullptr, root.string().c_str(), &si, &pi)) {
|
||||
if (CreateProcessA(cmdPath.string().c_str(), cmd.data(), nullptr, nullptr, TRUE, CREATE_NO_WINDOW, nullptr, root.string().c_str(), &si, &pi)) {
|
||||
CloseHandle(pi.hThread);
|
||||
hBuildProcess = pi.hProcess;
|
||||
isBuilding = true;
|
||||
|
|
@ -225,6 +243,8 @@ struct CodeEditorState {
|
|||
buildLog = "[ERROR] Не удалось запустить процесс сборки.\n";
|
||||
hasBuildResult = true;
|
||||
buildExitCode = 1;
|
||||
CloseHandle(hReadPipe);
|
||||
hReadPipe = nullptr;
|
||||
}
|
||||
CloseHandle(hWritePipe);
|
||||
}
|
||||
|
|
@ -316,9 +336,9 @@ struct CodeEditorState {
|
|||
static CodeEditorState g_CodeEditor;
|
||||
|
||||
void EditorApp::RenderCodeEditorWindow() {
|
||||
if (!showCodeEditorModal) return;
|
||||
|
||||
// Drain compiler output even when the IDE is hidden; a full pipe blocks ml/link.
|
||||
g_CodeEditor.PollBuild();
|
||||
if (!showCodeEditorModal) return;
|
||||
|
||||
ImGui::SetNextWindowSize(ImVec2(900, 650), ImGuiCond_FirstUseEver);
|
||||
if (!ImGui::Begin("Редактор кода и скриптов MASM32 - EblanStudio IDE", &showCodeEditorModal, ImGuiWindowFlags_MenuBar)) {
|
||||
|
|
@ -408,9 +428,6 @@ void EditorApp::RenderCodeEditorWindow() {
|
|||
for (int i = 0; i < g_CodeFileCount; ++i) {
|
||||
bool isSelected = (g_CodeEditor.currentFile == i);
|
||||
if (ImGui::Selectable(g_ProjectCodeFiles[i].displayName.c_str(), isSelected)) {
|
||||
if (g_CodeEditor.isModified) {
|
||||
g_CodeEditor.SaveFile(root);
|
||||
}
|
||||
g_CodeEditor.LoadFile(i, root);
|
||||
}
|
||||
}
|
||||
|
|
@ -512,7 +529,6 @@ void EditorApp::RenderCodeEditorWindow() {
|
|||
for (int f = 0; f < g_CodeFileCount; ++f) {
|
||||
if (err.file.find(g_ProjectCodeFiles[f].relativePath) != std::string::npos ||
|
||||
g_ProjectCodeFiles[f].relativePath.find(err.file) != std::string::npos) {
|
||||
if (g_CodeEditor.isModified) g_CodeEditor.SaveFile(root);
|
||||
g_CodeEditor.LoadFile(f, root);
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -103,7 +103,7 @@ struct SpriteEntry {
|
|||
|
||||
inline uint8_t GetSpriteEntityType(const SpriteEntry& s) {
|
||||
uint8_t t = static_cast<uint8_t>(s.flags & 0xFF);
|
||||
if (t == 0) {
|
||||
if (s.flags == 0) {
|
||||
if (s.textureId == 13) return ENTITY_ARCHETYPE_GUARD;
|
||||
if (s.textureId == 14) return ENTITY_ARCHETYPE_MEDKIT;
|
||||
if (s.textureId == 15) return ENTITY_ARCHETYPE_AMMO;
|
||||
|
|
@ -122,7 +122,10 @@ inline uint8_t GetSpriteAggro(const SpriteEntry& s) {
|
|||
uint8_t agg = static_cast<uint8_t>((s.flags >> 24) & 0xFF);
|
||||
return (agg == 0) ? 6 : agg;
|
||||
}
|
||||
inline void SetSpriteEntityData(SpriteEntry& s, uint8_t type, uint8_t health, uint8_t speed, uint8_t aggro) {
|
||||
// Clamp before narrowing: callers must not silently wrap values such as 256 HP.
|
||||
// Zero remains the legacy default encoding for health, speed and aggro.
|
||||
inline void SetSpriteEntityData(SpriteEntry& s, uint8_t type, int health, uint8_t speed, uint8_t aggro) {
|
||||
health = health < 0 ? 0 : (health > 255 ? 255 : health);
|
||||
s.flags = static_cast<uint32_t>(type) |
|
||||
(static_cast<uint32_t>(health) << 8) |
|
||||
(static_cast<uint32_t>(speed) << 16) |
|
||||
|
|
|
|||
BIN
engine.exe
BIN
engine.exe
Binary file not shown.
|
|
@ -10,6 +10,130 @@ DrawRectOutline PROTO :DWORD, :DWORD, :DWORD, :DWORD, :DWORD
|
|||
DrawString8x8 PROTO :DWORD, :DWORD, :DWORD, :DWORD
|
||||
DrawNumber8x8 PROTO :DWORD, :DWORD, :DWORD, :DWORD
|
||||
|
||||
; Trace the current world along the player's forward direction. EAX = first
|
||||
; blocking boundary in 24.8 world units. Glass blocks shots (unlike rendering).
|
||||
; No framebuffer/depth-buffer dependencies. Out-of-map cells are solid.
|
||||
TraceShotWall PROC uses ebx ecx edx esi edi
|
||||
LOCAL cellX:DWORD, cellY:DWORD, stepX:DWORD, stepY:DWORD
|
||||
LOCAL deltaX:DWORD, deltaY:DWORD, sideX:DWORD, sideY:DWORD
|
||||
LOCAL hitDistance:DWORD, remaining:DWORD
|
||||
|
||||
mov eax, playerX
|
||||
sar eax, 6
|
||||
mov cellX, eax
|
||||
mov eax, playerY
|
||||
sar eax, 6
|
||||
mov cellY, eax
|
||||
invoke GetMapCell, cellX, cellY
|
||||
invoke IsSolidCell, eax
|
||||
test eax, eax
|
||||
jnz shot_wall_at_origin
|
||||
|
||||
mov eax, playerAngle
|
||||
and eax, ANGLE_MASK
|
||||
mov esi, [tableCos+eax*4]
|
||||
mov edi, [tableSin+eax*4]
|
||||
mov deltaX, 3FFFFFFFh
|
||||
mov sideX, 3FFFFFFFh
|
||||
mov stepX, 1
|
||||
test esi, esi
|
||||
jz shot_setup_y
|
||||
mov eax, esi
|
||||
cdq
|
||||
xor eax, edx
|
||||
sub eax, edx
|
||||
mov ecx, eax
|
||||
mov eax, (64 shl 24)
|
||||
xor edx, edx
|
||||
div ecx
|
||||
mov deltaX, eax
|
||||
mov eax, playerX
|
||||
and eax, 63
|
||||
test esi, esi
|
||||
js shot_negative_x
|
||||
neg eax
|
||||
add eax, 64
|
||||
jmp shot_side_x
|
||||
shot_negative_x:
|
||||
mov stepX, -1
|
||||
shot_side_x:
|
||||
imul deltaX
|
||||
shrd eax, edx, 6
|
||||
mov sideX, eax
|
||||
shot_setup_y:
|
||||
mov deltaY, 3FFFFFFFh
|
||||
mov sideY, 3FFFFFFFh
|
||||
mov stepY, 1
|
||||
test edi, edi
|
||||
jz shot_traverse_start
|
||||
mov eax, edi
|
||||
cdq
|
||||
xor eax, edx
|
||||
sub eax, edx
|
||||
mov ecx, eax
|
||||
mov eax, (64 shl 24)
|
||||
xor edx, edx
|
||||
div ecx
|
||||
mov deltaY, eax
|
||||
mov eax, playerY
|
||||
and eax, 63
|
||||
test edi, edi
|
||||
js shot_negative_y
|
||||
neg eax
|
||||
add eax, 64
|
||||
jmp shot_side_y
|
||||
shot_negative_y:
|
||||
mov stepY, -1
|
||||
shot_side_y:
|
||||
imul deltaY
|
||||
shrd eax, edx, 6
|
||||
mov sideY, eax
|
||||
shot_traverse_start:
|
||||
mov remaining, MAP_W+MAP_H+2
|
||||
shot_traverse:
|
||||
mov eax, sideX
|
||||
cmp eax, sideY
|
||||
jb shot_step_x
|
||||
jne shot_step_y
|
||||
; At a corner also test the X-adjacent cell: no shots through wall seams.
|
||||
mov ebx, cellX
|
||||
add ebx, stepX
|
||||
invoke GetMapCell, ebx, cellY
|
||||
invoke IsSolidCell, eax
|
||||
test eax, eax
|
||||
jz shot_step_y
|
||||
mov eax, sideX
|
||||
ret
|
||||
shot_step_x:
|
||||
mov hitDistance, eax
|
||||
add eax, deltaX
|
||||
mov sideX, eax
|
||||
mov eax, stepX
|
||||
add cellX, eax
|
||||
jmp shot_test_cell
|
||||
shot_step_y:
|
||||
mov eax, sideY
|
||||
mov hitDistance, eax
|
||||
add eax, deltaY
|
||||
mov sideY, eax
|
||||
mov eax, stepY
|
||||
add cellY, eax
|
||||
shot_test_cell:
|
||||
invoke GetMapCell, cellX, cellY
|
||||
invoke IsSolidCell, eax
|
||||
test eax, eax
|
||||
jnz shot_wall_found
|
||||
dec remaining
|
||||
jnz shot_traverse
|
||||
; Defensive bound: a valid ray exits a 32x32 map within 64 crossings.
|
||||
shot_wall_found:
|
||||
mov eax, hitDistance
|
||||
ret
|
||||
shot_wall_at_origin:
|
||||
xor eax, eax
|
||||
ret
|
||||
TraceShotWall ENDP
|
||||
|
||||
; ------------------------------------------------------------------------------
|
||||
; UpdateCombat: Process weapon firing, hitscan raycasting, cooldowns & damage
|
||||
; ------------------------------------------------------------------------------
|
||||
|
|
@ -18,7 +142,11 @@ UpdateCombat PROC uses ebx ecx edx esi edi
|
|||
LOCAL i:DWORD, closestIdx:DWORD, closestDist:DWORD
|
||||
LOCAL sprX:DWORD, sprY:DWORD
|
||||
LOCAL transX:DWORD, transY:DWORD
|
||||
LOCAL sprScreenX:DWORD, wallDepth:DWORD
|
||||
LOCAL targetHalfWidth:DWORD, wallDepth:DWORD
|
||||
|
||||
call IsGameplayBlocked
|
||||
test eax, eax
|
||||
jnz combat_done
|
||||
|
||||
; 1. Decrement cooldowns
|
||||
cmp playerAttackCooldown, 0
|
||||
|
|
@ -63,8 +191,8 @@ UpdateCombat PROC uses ebx ecx edx esi edi
|
|||
mov edx, [tableSin + eax*4]
|
||||
mov camSin, edx
|
||||
|
||||
; Center wall depth from depthBuffer[160]
|
||||
mov eax, [depthBuffer + SCREEN_HALF_W * 4]
|
||||
; Query current geometry, never the previous rendered frame.
|
||||
call TraceShotWall
|
||||
mov wallDepth, eax
|
||||
|
||||
mov closestIdx, -1
|
||||
|
|
@ -117,8 +245,9 @@ hitscan_loop:
|
|||
cmp transY, 16
|
||||
jle next_hitscan_target
|
||||
|
||||
; Must be closer than the wall at center screen
|
||||
; Compare in 24.8 so a target on a wall boundary is not hit through it.
|
||||
mov eax, transY
|
||||
shl eax, 8
|
||||
cmp eax, wallDepth
|
||||
jge next_hitscan_target
|
||||
|
||||
|
|
@ -133,20 +262,21 @@ hitscan_loop:
|
|||
add ecx, eax
|
||||
mov transX, ecx
|
||||
|
||||
; sprScreenX = (transX * PROJ_DIST) / transY + SCREEN_HALF_W
|
||||
; World-space billboard hitbox: 32 units wide at scale 1.0, scaled with
|
||||
; the authored sprite, never with screen distance. Zero means legacy 1.0.
|
||||
mov eax, [ebx].SpriteStruct.scale
|
||||
test eax, eax
|
||||
jnz @F
|
||||
mov eax, FIXED_ONE
|
||||
@@:
|
||||
imul eax, 16
|
||||
sar eax, 16
|
||||
mov targetHalfWidth, eax
|
||||
mov eax, transX
|
||||
imul eax, PROJ_DIST
|
||||
cdq
|
||||
idiv transY
|
||||
add eax, SCREEN_HALF_W
|
||||
mov sprScreenX, eax
|
||||
|
||||
; Crosshair hitbox: within 22 pixels of center (SCREEN_HALF_W = 160)
|
||||
sub eax, SCREEN_HALF_W
|
||||
cdq
|
||||
xor eax, edx
|
||||
sub eax, edx ; abs(sprScreenX - 160)
|
||||
cmp eax, 22
|
||||
sub eax, edx
|
||||
cmp eax, targetHalfWidth
|
||||
jg next_hitscan_target
|
||||
|
||||
; Check if closer than previous target
|
||||
|
|
|
|||
|
|
@ -17,6 +17,9 @@ hEngineInstance dd 0
|
|||
isRunning dd 1
|
||||
|
||||
; High Precision Timing
|
||||
; Latched by UpdatePlayer: scene/end/skip/instant-transition ticks never run AI
|
||||
; or combat, even if they return control partway through the tick.
|
||||
gameplayTickBlocked dd 0
|
||||
targetFrameTimeMs dd 16 ; ~60 FPS (16.6 ms)
|
||||
lastTickCount dd 0
|
||||
fpsCounter dd 0
|
||||
|
|
|
|||
|
|
@ -142,6 +142,9 @@ UpdateEntities PROC uses ebx ecx edx esi edi
|
|||
ret
|
||||
|
||||
entities_active:
|
||||
call IsGameplayBlocked
|
||||
test eax, eax
|
||||
jnz entities_done
|
||||
mov i, 0
|
||||
|
||||
entity_loop:
|
||||
|
|
@ -376,6 +379,8 @@ check_guard_attack:
|
|||
test eax, eax
|
||||
jnz next_entity
|
||||
mov isPlayerDead, 1
|
||||
; Nothing else may collect items or attack after a lethal hit this tick.
|
||||
jmp entities_done
|
||||
|
||||
next_entity:
|
||||
inc i
|
||||
|
|
|
|||
|
|
@ -376,6 +376,9 @@ SwitchSublevel PROC uses esi edi, pszNextMap:DWORD, spawnX:DWORD, spawnY:DWORD,
|
|||
cmp fadeState, 0
|
||||
jne switch_busy
|
||||
|
||||
; Even an instant room switch consumes this tick. AI/combat must not use
|
||||
; pre-transition input in the newly loaded room.
|
||||
mov gameplayTickBlocked, 1
|
||||
mov esi, pszNextMap
|
||||
lea edi, pendingMapPath
|
||||
mov ecx, 32
|
||||
|
|
|
|||
|
|
@ -4,6 +4,17 @@
|
|||
|
||||
.code
|
||||
|
||||
; Shared gameplay gate. Returns EAX != 0 when simulation must be frozen.
|
||||
; UpdatePlayer owns the per-tick latch; renderer never changes this decision.
|
||||
IsGameplayBlocked PROC
|
||||
mov eax, gameplayTickBlocked
|
||||
or eax, isPlayerDead
|
||||
or eax, isCutsceneActive
|
||||
or eax, cutscenePending
|
||||
or eax, fadeState
|
||||
ret
|
||||
IsGameplayBlocked ENDP
|
||||
|
||||
; ------------------------------------------------------------------------------
|
||||
; UpdatePlayer: Updates rotation, computes movement vector & handles collisions
|
||||
; ------------------------------------------------------------------------------
|
||||
|
|
@ -13,6 +24,9 @@ UpdatePlayer PROC uses ebx ecx edx esi edi
|
|||
LOCAL nextX:DWORD, nextY:DWORD
|
||||
LOCAL checkGridX:DWORD, checkGridY:DWORD
|
||||
|
||||
mov gameplayTickBlocked, 1
|
||||
cmp isPlayerDead, 0
|
||||
jne update_player_done
|
||||
; Freeze player movement and inputs during level transitions
|
||||
cmp fadeState, 0
|
||||
jne update_player_done
|
||||
|
|
@ -37,6 +51,7 @@ scene_update_tick:
|
|||
call UpdateCutscene
|
||||
ret
|
||||
normal_player_update:
|
||||
mov gameplayTickBlocked, 0
|
||||
mov moveX, 0
|
||||
mov moveY, 0
|
||||
|
||||
|
|
@ -62,6 +77,8 @@ check_action:
|
|||
call InteractDoor
|
||||
cmp fadeState, 0
|
||||
jne update_player_done
|
||||
cmp gameplayTickBlocked, 0
|
||||
jne update_player_done
|
||||
|
||||
; 2. Determine Movement Speed
|
||||
check_movement:
|
||||
|
|
|
|||
|
|
@ -90,9 +90,13 @@ DrawRect ENDP
|
|||
; ------------------------------------------------------------------------------
|
||||
; DrawRectOutline: Draws a 1-pixel rectangle outline
|
||||
; ------------------------------------------------------------------------------
|
||||
DrawRectOutline PROC x:DWORD, y:DWORD, w:DWORD, h:DWORD, color:DWORD
|
||||
DrawRectOutline PROC uses ebx, x:DWORD, y:DWORD, w:DWORD, h:DWORD, color:DWORD
|
||||
LOCAL x2:DWORD, y2:DWORD
|
||||
|
||||
cmp w, 0
|
||||
jle outline_done
|
||||
cmp h, 0
|
||||
jle outline_done
|
||||
mov eax, x
|
||||
add eax, w
|
||||
dec eax
|
||||
|
|
@ -104,26 +108,28 @@ DrawRectOutline PROC x:DWORD, y:DWORD, w:DWORD, h:DWORD, color:DWORD
|
|||
mov y2, eax
|
||||
|
||||
; Top & Bottom horizontal lines
|
||||
mov ecx, x
|
||||
; DrawPixel may destroy EAX/ECX/EDX. Keep counters in callee-saved EBX.
|
||||
mov ebx, x
|
||||
@@:
|
||||
cmp ecx, x2
|
||||
cmp ebx, x2
|
||||
jg @F
|
||||
invoke DrawPixel, ecx, y, color
|
||||
invoke DrawPixel, ecx, y2, color
|
||||
inc ecx
|
||||
invoke DrawPixel, ebx, y, color
|
||||
invoke DrawPixel, ebx, y2, color
|
||||
inc ebx
|
||||
jmp @B
|
||||
@@:
|
||||
|
||||
; Left & Right vertical lines
|
||||
mov edx, y
|
||||
mov ebx, y
|
||||
@@:
|
||||
cmp edx, y2
|
||||
cmp ebx, y2
|
||||
jg @F
|
||||
invoke DrawPixel, x, edx, color
|
||||
invoke DrawPixel, x2, edx, color
|
||||
inc edx
|
||||
invoke DrawPixel, x, ebx, color
|
||||
invoke DrawPixel, x2, ebx, color
|
||||
inc ebx
|
||||
jmp @B
|
||||
@@:
|
||||
outline_done:
|
||||
ret
|
||||
DrawRectOutline ENDP
|
||||
|
||||
|
|
|
|||
BIN
main.obj
BIN
main.obj
Binary file not shown.
|
|
@ -23,6 +23,7 @@ static std::vector<uint8_t> Bytes(const fs::path& path) {
|
|||
|
||||
#include "cutscene_editor_checks.inl"
|
||||
#include "viewport_editor_checks.inl"
|
||||
#include "stability_editor_checks.inl"
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
EditorApp app;
|
||||
|
|
@ -152,6 +153,7 @@ int main(int argc, char** argv) {
|
|||
app.CleanupPlaytest();
|
||||
}
|
||||
if(argc>2) RunViewportChecks(app,argv[2]);
|
||||
RunStabilityEditorChecks();
|
||||
std::cout << "Editor: " << checks - failures << '/' << checks << " passed\n";
|
||||
return failures ? 1 : 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -126,6 +126,7 @@ state_changed:
|
|||
StateUnchanged ENDP
|
||||
|
||||
include tests/cutscene_engine_checks.inc
|
||||
include tests/stability_engine_checks.inc
|
||||
|
||||
AuditMain PROC
|
||||
call InitMathTables
|
||||
|
|
@ -363,6 +364,7 @@ flat_depth_ok:
|
|||
invoke Dump, OFFSET projectFramePath, OFFSET screenBuffer, FRAME_BUFFER_SIZE
|
||||
|
||||
call RunCutsceneChecks
|
||||
call RunStabilityChecks
|
||||
mov eax, resultCount
|
||||
inc eax
|
||||
shl eax, 2
|
||||
|
|
|
|||
66
tests/stability_editor_checks.inl
Normal file
66
tests/stability_editor_checks.inl
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
static void RunStabilityEditorChecks() {
|
||||
SpriteEntry sprite{};
|
||||
sprite.textureId = 13;
|
||||
Check(GetSpriteEntityType(sprite) == ENTITY_ARCHETYPE_GUARD, "zero flags retain legacy guard fallback");
|
||||
sprite.flags = 0x06026400;
|
||||
Check(GetSpriteEntityType(sprite) == ENTITY_ARCHETYPE_PROP, "explicit Prop matches ASM despite guard texture");
|
||||
for (int health : {1, 100, 255, 256, 300, 500}) {
|
||||
SetSpriteEntityData(sprite, ENTITY_ARCHETYPE_GUARD, health, 2, 6);
|
||||
Check(GetSpriteHealth(sprite) == (health > 255 ? 255 : health), "health setter clamps before byte packing");
|
||||
}
|
||||
|
||||
const auto root = fs::absolute("code fixture");
|
||||
fs::create_directories(root / "include");
|
||||
const auto first = root / g_ProjectCodeFiles[0].relativePath;
|
||||
const auto second = root / g_ProjectCodeFiles[1].relativePath;
|
||||
{ std::ofstream out(first); out << "original"; }
|
||||
{ std::ofstream out(second); out << "second"; }
|
||||
CodeEditorState code;
|
||||
code.LoadFile(0, root.string());
|
||||
Check(std::string(code.textBuffer.data()) == "original", "IDE opens actual source file");
|
||||
strcpy_s(code.textBuffer.data(), code.textBuffer.size(), "unsaved source");
|
||||
code.isModified = true;
|
||||
HANDLE locked = CreateFileA(first.string().c_str(), GENERIC_READ, FILE_SHARE_READ,
|
||||
nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||
Check(locked != INVALID_HANDLE_VALUE, "IDE failure fixture locks destination against replacement");
|
||||
Check(!code.SaveFile(root.string()) && code.isModified &&
|
||||
Bytes(first) == std::vector<uint8_t>({'o','r','i','g','i','n','a','l'}),
|
||||
"failed IDE replacement preserves disk and modified buffer");
|
||||
code.LoadFile(1, root.string());
|
||||
Check(code.currentFile == 0 && code.isModified && std::string(code.textBuffer.data()) == "unsaved source",
|
||||
"IDE refuses file switch after failed autosave");
|
||||
code.StartBuild(root.string());
|
||||
Check(!code.isBuilding && code.isModified, "IDE refuses build after failed save");
|
||||
CloseHandle(locked);
|
||||
Check(code.SaveFile(root.string()) && !code.isModified &&
|
||||
Bytes(first) == std::vector<uint8_t>({'u','n','s','a','v','e','d',' ','s','o','u','r','c','e'}),
|
||||
"IDE publishes complete source atomically");
|
||||
code.LoadFile(1, root.string());
|
||||
Check(code.currentFile == 1 && std::string(code.textBuffer.data()) == "second", "IDE switches after successful save");
|
||||
fs::remove(first);
|
||||
code.LoadFile(0, root.string());
|
||||
Check(code.currentFile == 1 && std::string(code.textBuffer.data()) == "second", "failed IDE load preserves current document");
|
||||
Check(std::distance(fs::directory_iterator(root / "include"), fs::directory_iterator{}) == 1,
|
||||
"failed IDE saves leave no temporary source files");
|
||||
|
||||
{ std::ofstream out(root / "build.bat");
|
||||
out << "@echo off\nfor /L %%i in (1,1,4000) do echo compiler-output-%%i\nexit /b 7\n"; }
|
||||
g_CodeEditor.LoadFile(1, root.string());
|
||||
g_CodeEditor.StartBuild(root.string());
|
||||
EditorApp hidden;
|
||||
hidden.showCodeEditorModal = false;
|
||||
const auto deadline = GetTickCount64() + 10000;
|
||||
while (g_CodeEditor.isBuilding && GetTickCount64() < deadline) {
|
||||
hidden.RenderCodeEditorWindow();
|
||||
if (g_CodeEditor.hBuildProcess) WaitForSingleObject(g_CodeEditor.hBuildProcess, 1);
|
||||
}
|
||||
const bool finished = !g_CodeEditor.isBuilding;
|
||||
if (!finished && g_CodeEditor.hBuildProcess) {
|
||||
TerminateProcess(g_CodeEditor.hBuildProcess, 99);
|
||||
WaitForSingleObject(g_CodeEditor.hBuildProcess, 1000);
|
||||
g_CodeEditor.PollBuild();
|
||||
}
|
||||
Check(finished && g_CodeEditor.buildExitCode == 7 &&
|
||||
g_CodeEditor.buildLog.find("compiler-output-4000") != std::string::npos,
|
||||
"hidden IDE drains output larger than pipe capacity and retains exit code");
|
||||
}
|
||||
202
tests/stability_engine_checks.inc
Normal file
202
tests/stability_engine_checks.inc
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
; Real gameplay procedures, deliberately without Render3DScene between shots.
|
||||
.code
|
||||
ResetCombatFixture PROC uses eax ecx edi
|
||||
mov edi, OFFSET gameMap
|
||||
mov ecx, MAP_SIZE
|
||||
xor eax, eax
|
||||
rep stosb
|
||||
mov fadeState, 0
|
||||
mov isCutsceneActive, 0
|
||||
mov cutscenePending, 0
|
||||
mov isPlayerDead, 0
|
||||
mov playerHealth, 100
|
||||
mov playerAmmo, 50
|
||||
mov playerScore, 0
|
||||
mov playerAttackCooldown, 0
|
||||
mov playerFlashTimer, 0
|
||||
mov playerHitFlash, 0
|
||||
mov playerX, 160
|
||||
mov playerY, 160
|
||||
mov playerAngle, 0
|
||||
mov keyForward, 0
|
||||
mov keyBackward, 0
|
||||
mov keyStrafeLeft, 0
|
||||
mov keyStrafeRight, 0
|
||||
mov keyTurnLeft, 0
|
||||
mov keyTurnRight, 0
|
||||
mov keyCutscene, 0
|
||||
mov keySkipScene, 0
|
||||
mov keyAction, 0
|
||||
mov keyRestart, 0
|
||||
mov keyAttack, 1
|
||||
mov transitionCount, 0
|
||||
mov spriteCount, 1
|
||||
mov [spriteList].SpriteStruct.x, 288
|
||||
mov [spriteList].SpriteStruct.y, 160
|
||||
mov [spriteList].SpriteStruct.textureId, 13
|
||||
mov [spriteList].SpriteStruct.scale, FIXED_ONE
|
||||
mov [spriteList].SpriteStruct.flags, 0
|
||||
call InitEntities
|
||||
call UpdatePlayer
|
||||
ret
|
||||
ResetCombatFixture ENDP
|
||||
|
||||
RunStabilityChecks PROC
|
||||
mov edi, OFFSET screenBuffer
|
||||
mov ecx, FRAME_BUFFER_SIZE/4
|
||||
mov eax, 00123456h
|
||||
rep stosd
|
||||
invoke DrawRectOutline, 68, 42, 60, 8, COLOR_WHITE
|
||||
CHECK DWORD PTR [screenBuffer+42*SCREEN_PITCH+127*4], COLOR_WHITE ; white outline reaches top right
|
||||
CHECK DWORD PTR [screenBuffer+49*SCREEN_PITCH+100*4], COLOR_WHITE ; white outline draws bottom edge
|
||||
CHECK DWORD PTR [screenBuffer+45*SCREEN_PITCH+68*4], COLOR_WHITE ; white outline draws left edge
|
||||
CHECK DWORD PTR [screenBuffer+45*SCREEN_PITCH+100*4], 00123456h ; outline keeps interior unchanged
|
||||
invoke DrawRectOutline, 68, 42, 60, 8, 0
|
||||
CHECK DWORD PTR [screenBuffer+49*SCREEN_PITCH+100*4], 0 ; black outline terminates and draws bottom
|
||||
invoke DrawRectOutline, -2, -2, 5, 5, COLOR_GREEN
|
||||
CHECK DWORD PTR [screenBuffer+2*SCREEN_PITCH], COLOR_GREEN ; outline clips offscreen coordinates
|
||||
invoke DrawRectOutline, 100, 45, 0, 3, COLOR_WHITE
|
||||
CHECK DWORD PTR [screenBuffer+45*SCREEN_PITCH+100*4], 00123456h ; zero width outline draws nothing
|
||||
|
||||
call ResetCombatFixture
|
||||
mov DWORD PTR [depthBuffer+160*4], 0
|
||||
call UpdateCombat
|
||||
CHECK entityHealth, 65 ; hitscan works without a rendered depth buffer
|
||||
CHECK playerAmmo, 49 ; one shot consumes one round
|
||||
mov playerAttackCooldown, 0
|
||||
mov byte ptr [gameMap+2*MAP_W+3], CELL_BRICK
|
||||
mov DWORD PTR [depthBuffer+160*4], 999999
|
||||
call UpdateCombat
|
||||
CHECK entityHealth, 65 ; newly placed wall blocks a shot despite stale depth
|
||||
mov playerAttackCooldown, 0
|
||||
mov byte ptr [gameMap+2*MAP_W+3], CELL_DOOR_OPEN
|
||||
call UpdateCombat
|
||||
CHECK entityHealth, 30 ; newly opened door permits a shot without rendering
|
||||
mov playerAttackCooldown, 0
|
||||
mov byte ptr [gameMap+2*MAP_W+3], CELL_GLASS
|
||||
call UpdateCombat
|
||||
CHECK entityHealth, 30 ; solid glass blocks gameplay shots
|
||||
|
||||
call ResetCombatFixture
|
||||
mov [spriteList].SpriteStruct.x, 800
|
||||
mov [spriteList].SpriteStruct.y, 180
|
||||
mov DWORD PTR [depthBuffer+160*4], 999999
|
||||
call UpdateCombat
|
||||
CHECK entityHealth, 100 ; distant target does not acquire a larger world hitbox
|
||||
call ResetCombatFixture
|
||||
mov [spriteList].SpriteStruct.x, 160
|
||||
mov [spriteList].SpriteStruct.y, 288
|
||||
mov playerAngle, ANGLE_90
|
||||
mov DWORD PTR [depthBuffer+160*4], 0
|
||||
call UpdateCombat
|
||||
CHECK entityHealth, 65 ; cardinal turn uses current camera instead of previous frame
|
||||
call ResetCombatFixture
|
||||
mov playerAngle, ANGLE_180
|
||||
mov [spriteList].SpriteStruct.x, 32
|
||||
call UpdateCombat
|
||||
CHECK entityHealth, 65 ; negative cardinal shot traverses the current map
|
||||
call ResetCombatFixture
|
||||
mov playerAngle, ANGLE_45
|
||||
mov [spriteList].SpriteStruct.y, 288
|
||||
call UpdateCombat
|
||||
CHECK entityHealth, 65 ; diagonal shot reaches unobstructed target
|
||||
mov playerAttackCooldown, 0
|
||||
mov byte ptr [gameMap+2*MAP_W+3], CELL_BRICK
|
||||
call UpdateCombat
|
||||
CHECK entityHealth, 65 ; diagonal ray cannot pass through a wall corner
|
||||
call ResetCombatFixture
|
||||
mov spriteCount, 2
|
||||
mov esi, OFFSET spriteList+sizeof SpriteStruct
|
||||
mov [esi].SpriteStruct.x, 224
|
||||
mov [esi].SpriteStruct.y, 160
|
||||
mov [esi].SpriteStruct.textureId, 13
|
||||
mov [esi].SpriteStruct.scale, FIXED_ONE
|
||||
mov [esi].SpriteStruct.flags, 0
|
||||
call InitEntities
|
||||
call UpdateCombat
|
||||
CHECK entityHealth, 100 ; hitscan does not damage farther overlapping guard
|
||||
CHECK entityHealth[4], 65 ; hitscan selects nearest live guard independent of insertion order
|
||||
|
||||
call ResetCombatFixture
|
||||
mov [spriteList].SpriteStruct.y, 180
|
||||
mov [spriteList].SpriteStruct.scale, 2*FIXED_ONE
|
||||
call UpdateCombat
|
||||
CHECK entityHealth, 65 ; authored sprite scale affects world-space hitbox
|
||||
mov playerAttackCooldown, 0
|
||||
mov [spriteList].SpriteStruct.scale, 0
|
||||
call UpdateCombat
|
||||
CHECK entityHealth, 65 ; zero sprite scale retains legacy one-unit scale
|
||||
|
||||
call ResetCombatFixture
|
||||
mov isCutsceneActive, 1
|
||||
mov playerAttackCooldown, 4
|
||||
call UpdateEntities
|
||||
call UpdateCombat
|
||||
CHECK [spriteList].SpriteStruct.x, 288 ; cinematic camera cannot attract guards
|
||||
CHECK playerAmmo, 50 ; cinematic camera cannot fire
|
||||
CHECK playerAttackCooldown, 4 ; cinematic pauses weapon timers
|
||||
mov isCutsceneActive, 0
|
||||
mov fadeState, 1
|
||||
call UpdateEntities
|
||||
call UpdateCombat
|
||||
CHECK [spriteList].SpriteStruct.x, 288 ; transition freezes guards
|
||||
CHECK playerAmmo, 50 ; transition blocks firing
|
||||
mov fadeState, 0
|
||||
mov cutscenePending, 1
|
||||
call UpdateEntities
|
||||
call UpdateCombat
|
||||
CHECK [spriteList].SpriteStruct.x, 288 ; pending autoplay blocks gameplay before first scene tick
|
||||
CHECK playerAmmo, 50 ; pending autoplay blocks firing
|
||||
|
||||
call ResetCombatFixture
|
||||
mov isCutsceneActive, 1
|
||||
mov keySkipScene, 1
|
||||
call UpdatePlayer
|
||||
call UpdateEntities
|
||||
call UpdateCombat
|
||||
CHECK isCutsceneActive, 0 ; skip stops cinematic playback
|
||||
CHECK [spriteList].SpriteStruct.x, 288 ; skip tick does not resume guards midway through tick
|
||||
CHECK playerAmmo, 50 ; skip tick cannot fire stale scene input
|
||||
call ResetCombatFixture
|
||||
invoke SwitchSublevel, OFFSET arenaPath, 2, 2, 0, 0
|
||||
call UpdateCombat
|
||||
CHECK playerAmmo, 50 ; instant transition cannot fire into newly loaded room
|
||||
|
||||
call ResetCombatFixture
|
||||
mov [spriteList].SpriteStruct.x, 160
|
||||
mov [spriteList].SpriteStruct.textureId, 14
|
||||
call InitEntities
|
||||
mov playerHealth, 50
|
||||
mov isCutsceneActive, 1
|
||||
call UpdateEntities
|
||||
CHECK playerHealth, 50 ; cinematic camera cannot collect a medkit
|
||||
CHECK [spriteList].SpriteStruct.textureId, 14 ; cinematic leaves pickup active
|
||||
mov isCutsceneActive, 0
|
||||
call UpdateEntities
|
||||
CHECK playerHealth, 75 ; normal gameplay collects medkit
|
||||
|
||||
call ResetCombatFixture
|
||||
mov isPlayerDead, 1
|
||||
mov keyForward, 1
|
||||
mov keyTurnRight, 1
|
||||
call UpdatePlayer
|
||||
call UpdateCombat
|
||||
CHECK playerX, 160 ; death blocks movement
|
||||
CHECK playerAngle, 0 ; death blocks turning
|
||||
CHECK playerAmmo, 50 ; death blocks firing
|
||||
mov isPlayerDead, 0
|
||||
mov keyForward, 0
|
||||
mov keyTurnRight, 0
|
||||
call UpdatePlayer
|
||||
|
||||
mov [spriteList].SpriteStruct.flags, 0602FF01h
|
||||
call InitEntities
|
||||
CHECK entityHealth, 255 ; packed guard health preserves maximum byte
|
||||
mov [spriteList].SpriteStruct.flags, 06026400h
|
||||
call InitEntities
|
||||
CHECK entityType, ENTITY_TYPE_PROP ; nonzero packed Prop never falls back to texture
|
||||
mov [spriteList].SpriteStruct.flags, 0
|
||||
call InitEntities
|
||||
CHECK entityType, ENTITY_TYPE_GUARD ; zero flags keep legacy texture fallback
|
||||
ret
|
||||
RunStabilityChecks ENDP
|
||||
|
|
@ -133,7 +133,7 @@ def main():
|
|||
(work / "project_paths.inc").write_text(
|
||||
f'.data\nprojectMapPath db "{ROOT / "assets/map.dat"}",0\n'
|
||||
f'projectPackPath db "{ROOT / "assets/textures/texturepack.bin"}",0\n.code\n')
|
||||
masm_candidates = [Path(p) for p in ("D:/MASM32/masm32/bin/ml.exe", "C:/masm32/bin/ml.exe", "F:/MASM32/masm32/bin/ml.exe")]
|
||||
masm_candidates = [Path(p) for p in ("E:/MASM32/masm32/bin/ml.exe", "D:/MASM32/masm32/bin/ml.exe", "C:/masm32/bin/ml.exe", "F:/MASM32/masm32/bin/ml.exe")]
|
||||
masm_candidates += list(Path("C:/Program Files (x86)/Microsoft Visual Studio").glob("*/BuildTools/VC/Tools/MSVC/*/bin/Hostx64/x86/ml.exe"))
|
||||
ml = next((p for p in masm_candidates if p.is_file()), None)
|
||||
if not ml:
|
||||
|
|
@ -148,7 +148,9 @@ def main():
|
|||
f"/OUT:{work / 'engine_tests.exe'}", str(work / "engine_tests.obj"),
|
||||
"kernel32.lib", "user32.lib", "gdi32.lib", "winmm.lib"], work)
|
||||
run([str(work / "engine_tests.exe"), "pace map.dat"], work)
|
||||
source = (ROOT / "tests/engine_regressions.asm").read_text() + (ROOT / "tests/cutscene_engine_checks.inc").read_text()
|
||||
source = (ROOT / "tests/engine_regressions.asm").read_text() + "".join(
|
||||
(ROOT / "tests" / name).read_text() for name in
|
||||
("cutscene_engine_checks.inc", "stability_engine_checks.inc"))
|
||||
names = re.findall(r"^\s+CHECK .*?; (.+)$", source, re.M)
|
||||
raw = (work / "engine_results.bin").read_bytes()
|
||||
values = struct.unpack(f"<{len(raw)//4}I", raw)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue