Compare commits
2 commits
2bac213ac5
...
527d92411d
| Author | SHA1 | Date | |
|---|---|---|---|
| 527d92411d | |||
| f42679fe62 |
21 changed files with 777 additions and 101 deletions
124
docs/STABILIZATION.md
Normal file
124
docs/STABILIZATION.md
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
# 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.
|
||||
|
||||
## Preparation for stage 2 — integration with current main
|
||||
|
||||
Integrated server history through `2bac213` with stage-1 commit `f42679f` in an
|
||||
isolated worktree, preserving both histories and the original working directory.
|
||||
Retained map v5, preview IPC v2, authored lighting, three weapons, explosive
|
||||
barrels, modal dialogues, peaceful mode and the updated studio UI.
|
||||
|
||||
Compatibility adjustments:
|
||||
|
||||
- Pause and modal dialogue join the shared gameplay gate, including direct calls
|
||||
to gameplay procedures. Blocked player updates cannot activate portal triggers.
|
||||
- Knife/pistol/shotgun keep their range, damage, ammunition and reserved light
|
||||
behavior. World-space half-widths are respectively 24/16/32 times sprite scale;
|
||||
the shotgun remains a single-target wider hit test, not three separate rays.
|
||||
- Barrel texture fallback is enabled only with zero flags in both editor and ASM.
|
||||
Explicit Prop overrides barrel artwork and cannot explode.
|
||||
- A new explosion regression reproduced access violation `0xC0000005`: the AOE
|
||||
loop kept a sprite pointer in EDX and destroyed it with CDQ. The pointer now
|
||||
stays in EBX. A nearby guard receives blast damage without crashing.
|
||||
- IDE tab navigation, API catalog and docked view retain stage-1 safe writes.
|
||||
|
||||
Verification: both production builds succeeded; editor **137/137**, ASM
|
||||
**191/191**. All 1024 baked RGB cells match between C++ and ASM; classic and lit
|
||||
preview frames match direct ASM rendering, including the x86/x64/DX11 path.
|
||||
Project assets remain byte-for-byte unchanged. The original working directory
|
||||
still has only its pre-existing `editor/imgui.ini` modification. No separate
|
||||
interactive UI playtest was performed during this preparation.
|
||||
|
||||
### Stage 2 implementation contract (not implemented here)
|
||||
|
||||
1. Separate message/input polling, fixed simulation ticks and drawing/presentation.
|
||||
Use QPC and a 60 Hz accumulator with bounded catch-up. Consume edge-triggered
|
||||
commands once, not once per catch-up tick. Reset timing debt after pause/focus
|
||||
stalls; advance world timers/lights only on the appropriate simulation tick.
|
||||
2. Keep movement fractions in runtime state without changing serialized raw-unit
|
||||
coordinates. Cancel opposing inputs, normalize combined forward/strafe input,
|
||||
and use symmetric signed accumulation. Clear fractions on load/teleport/restart.
|
||||
3. Check the full collision body on each axis and preserve wall sliding; prevent
|
||||
corner clipping and occupied-door closure. Keep portals intentionally walkable.
|
||||
4. Centralize Playing/Cutscene/Transition/Dead/Paused/Dialog update rules, including
|
||||
`Game_OnTick`, dynamic lights, fade, toasts and modal dismissal. Render routines
|
||||
must not advance those timers or perform input-driven state changes.
|
||||
5. Separate cinematic camera pose from gameplay/player position. Define END/skip
|
||||
handoff explicitly and validate the restored gameplay collision body, including
|
||||
paths crossing walls. Keep C++ preview and ASM cinematic traces aligned.
|
||||
6. Track the current room path and its loaded entry spawn separately from startup
|
||||
map. Restart restores that room and entry pose, resets gameplay transients and
|
||||
fractional movement, and preserves project mode/settings. Failed reload must
|
||||
not partially reset the current room. Make death-R and pause restart share it.
|
||||
|
||||
Acceptance checks: identical simulation after 60 ticks under different render
|
||||
schedules; single consumption of edge inputs during catch-up; symmetric movement
|
||||
for all quadrants and equal cardinal/diagonal speed; collision corner/slide cases;
|
||||
no simulation advancement during pause/dialog; no pickups/AI along camera paths;
|
||||
safe cinematic handoff; restart after a room transition; failed restart rollback;
|
||||
unchanged v1..v5 loading, lighting bake parity and IPC v2 framebuffer parity.
|
||||
|
||||
## 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.
|
|
@ -2913,18 +2913,18 @@ void EditorApp::RenderInspector() {
|
|||
|
||||
if (ImGui::Combo("Архетип сущности", &archType, s_ArchetypeNames, 7)) {
|
||||
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));
|
||||
}
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), "● Отображается красным кругом на 2D карте");
|
||||
} else if (archType == ENTITY_ARCHETYPE_MEDKIT) {
|
||||
|
|
|
|||
|
|
@ -177,6 +177,7 @@ static const int g_EngineApiCount = sizeof(g_EngineApiCatalog) / sizeof(g_Engine
|
|||
struct CodeEditorState {
|
||||
int currentFile = 0;
|
||||
std::string fileContent;
|
||||
fs::path loadedPath;
|
||||
std::vector<char> textBuffer;
|
||||
bool isModified = false;
|
||||
std::string statusText = "Готов к редактированию кода MASM32";
|
||||
|
|
@ -208,50 +209,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;
|
||||
|
|
@ -261,7 +276,7 @@ struct CodeEditorState {
|
|||
if (isBuilding) return;
|
||||
|
||||
// Auto-save current file before building
|
||||
SaveFile(projectRoot);
|
||||
if (!SaveFile(projectRoot)) return;
|
||||
|
||||
buildLog.clear();
|
||||
buildErrors.clear();
|
||||
|
|
@ -303,9 +318,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;
|
||||
|
|
@ -314,6 +332,8 @@ struct CodeEditorState {
|
|||
buildLog = "[ERROR] Не удалось запустить процесс сборки.\n";
|
||||
hasBuildResult = true;
|
||||
buildExitCode = 1;
|
||||
CloseHandle(hReadPipe);
|
||||
hReadPipe = nullptr;
|
||||
}
|
||||
CloseHandle(hWritePipe);
|
||||
}
|
||||
|
|
@ -404,9 +424,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(1020, 680), ImGuiCond_FirstUseEver);
|
||||
if (!ImGui::Begin("Редактор кода MASM32 & Игровых скриптов (F7)", &showCodeEditorModal, ImGuiWindowFlags_MenuBar)) {
|
||||
|
|
@ -523,9 +543,6 @@ void EditorApp::RenderCodeEditorContents(const std::string& root) {
|
|||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -656,7 +673,6 @@ void EditorApp::RenderCodeEditorContents(const std::string& root) {
|
|||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -154,7 +154,7 @@ void EditorApp::RenderLightingInspector() {
|
|||
|
||||
ImGui::Spacing();
|
||||
ImGui::TextColored(ImVec4(0.4f, 0.85f, 1.0f, 1.0f), "Быстрые пресеты источников:");
|
||||
|
||||
|
||||
// Preset Buttons: Clean 3-Column Grid (all 7 presets visible!)
|
||||
float colW3 = (ImGui::GetContentRegionAvail().x - 12.0f) / 3.0f;
|
||||
if (ImGui::Button("Факел", ImVec2(colW3, 24))) ApplyLightPreset(selectedLightIdx, LIGHT_PRESET_TORCH);
|
||||
|
|
|
|||
|
|
@ -105,7 +105,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 == 11) return ENTITY_ARCHETYPE_BARREL;
|
||||
if (s.textureId == 13) return ENTITY_ARCHETYPE_GUARD;
|
||||
if (s.textureId == 14) return ENTITY_ARCHETYPE_MEDKIT;
|
||||
|
|
@ -125,7 +125,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.
|
|
@ -12,6 +12,130 @@ DrawNumber8x8 PROTO :DWORD, :DWORD, :DWORD, :DWORD
|
|||
ShowGameToast PROTO :DWORD, :DWORD, :DWORD
|
||||
Game_OnEntityKilled PROTO :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
|
||||
; ------------------------------------------------------------------------------
|
||||
|
|
@ -20,8 +144,12 @@ 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 weaponDamage:DWORD, maxHitDist:DWORD, hitTolerance:DWORD
|
||||
LOCAL targetHalfWidth:DWORD, wallDepth:DWORD
|
||||
LOCAL weaponDamage:DWORD, maxHitDist:DWORD, hitHalfWidth:DWORD
|
||||
|
||||
call IsGameplayBlocked
|
||||
test eax, eax
|
||||
jnz combat_done
|
||||
|
||||
; 1. Decrement cooldowns
|
||||
cmp playerAttackCooldown, 0
|
||||
|
|
@ -58,7 +186,7 @@ UpdateCombat PROC uses ebx ecx edx esi edi
|
|||
; Set weapon parameters based on playerWeapon
|
||||
mov weaponDamage, 35
|
||||
mov maxHitDist, 999999
|
||||
mov hitTolerance, 22
|
||||
mov hitHalfWidth, 16
|
||||
|
||||
cmp playerWeapon, WEAPON_KNIFE
|
||||
je setup_knife
|
||||
|
|
@ -70,32 +198,32 @@ setup_knife:
|
|||
; Knife: no ammo needed, close range (within 64 units = 1 cell), fast cooldown
|
||||
mov weaponDamage, 30
|
||||
mov maxHitDist, 64
|
||||
mov hitTolerance, 32
|
||||
mov hitHalfWidth, 24
|
||||
mov playerAttackCooldown, 8
|
||||
mov playerFlashTimer, 3
|
||||
jmp fire_hitscan
|
||||
|
||||
setup_shotgun:
|
||||
; Shotgun: needs 2 ammo, high damage (70), wide spread (hitTolerance 40), long cooldown
|
||||
; Shotgun: needs 2 ammo, high damage (70), wider world hitbox, long cooldown
|
||||
cmp playerAmmo, 2
|
||||
jl combat_dry_fire
|
||||
sub playerAmmo, 2
|
||||
mov weaponDamage, 70
|
||||
mov maxHitDist, 999999
|
||||
mov hitTolerance, 40
|
||||
mov hitHalfWidth, 32
|
||||
mov playerAttackCooldown, 22
|
||||
mov playerFlashTimer, 7
|
||||
invoke SpawnMuzzleFlashLight, playerX, playerY
|
||||
jmp fire_hitscan
|
||||
|
||||
setup_pistol:
|
||||
; Pistol: needs 1 ammo, accurate (hitTolerance 22), medium cooldown (12)
|
||||
; Pistol: needs 1 ammo, accurate world hitbox, medium cooldown (12)
|
||||
cmp playerAmmo, 1
|
||||
jl combat_dry_fire
|
||||
dec playerAmmo
|
||||
mov weaponDamage, 35
|
||||
mov maxHitDist, 999999
|
||||
mov hitTolerance, 22
|
||||
mov hitHalfWidth, 16
|
||||
mov playerAttackCooldown, 12
|
||||
mov playerFlashTimer, 5
|
||||
invoke SpawnMuzzleFlashLight, playerX, playerY
|
||||
|
|
@ -111,8 +239,8 @@ fire_hitscan:
|
|||
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
|
||||
|
|
@ -133,14 +261,6 @@ hitscan_loop:
|
|||
cmp eax, ENTITY_TYPE_BARREL
|
||||
je hitscan_target_valid
|
||||
|
||||
; Also check if prop has barrel texture (11)
|
||||
mov ebx, i
|
||||
imul ebx, sizeof SpriteStruct
|
||||
add ebx, OFFSET spriteList
|
||||
mov eax, [ebx].SpriteStruct.textureId
|
||||
cmp eax, 11
|
||||
je hitscan_target_valid
|
||||
|
||||
jmp next_hitscan_target
|
||||
|
||||
hitscan_target_valid:
|
||||
|
|
@ -185,8 +305,9 @@ hitscan_target_valid:
|
|||
cmp eax, maxHitDist
|
||||
jg 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
|
||||
|
||||
|
|
@ -201,20 +322,21 @@ hitscan_target_valid:
|
|||
add ecx, eax
|
||||
mov transX, ecx
|
||||
|
||||
; sprScreenX = (transX * PROJ_DIST) / transY + SCREEN_HALF_W
|
||||
; World-space billboard hitbox: weapon-specific width, 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, hitHalfWidth
|
||||
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 check
|
||||
sub eax, SCREEN_HALF_W
|
||||
cdq
|
||||
xor eax, edx
|
||||
sub eax, edx ; abs(sprScreenX - 160)
|
||||
cmp eax, hitTolerance
|
||||
sub eax, edx
|
||||
cmp eax, targetHalfWidth
|
||||
jg next_hitscan_target
|
||||
|
||||
; Check if closer than previous target
|
||||
|
|
@ -239,12 +361,6 @@ hitscan_eval:
|
|||
; Check if hit an explosive barrel!
|
||||
cmp eax, ENTITY_TYPE_BARREL
|
||||
je explode_barrel
|
||||
mov ebx, closestIdx
|
||||
imul ebx, sizeof SpriteStruct
|
||||
add ebx, OFFSET spriteList
|
||||
cmp [ebx].SpriteStruct.textureId, 11
|
||||
je explode_barrel
|
||||
|
||||
; Hit an enemy guard!
|
||||
mov eax, [entityHealth + ecx*4]
|
||||
sub eax, weaponDamage
|
||||
|
|
@ -308,11 +424,12 @@ aoe_loop:
|
|||
cmp [entityState + ecx*4], ENTITY_STATE_DEAD
|
||||
je next_aoe
|
||||
|
||||
mov edx, i
|
||||
imul edx, sizeof SpriteStruct
|
||||
add edx, OFFSET spriteList
|
||||
; EDX is volatile scratch for CDQ below, not a safe sprite pointer.
|
||||
mov ebx, i
|
||||
imul ebx, sizeof SpriteStruct
|
||||
add ebx, OFFSET spriteList
|
||||
|
||||
mov eax, [edx].SpriteStruct.x
|
||||
mov eax, [ebx].SpriteStruct.x
|
||||
sub eax, esi
|
||||
cdq
|
||||
xor eax, edx
|
||||
|
|
@ -320,7 +437,7 @@ aoe_loop:
|
|||
cmp eax, 128
|
||||
jg next_aoe
|
||||
|
||||
mov eax, [edx].SpriteStruct.y
|
||||
mov eax, [ebx].SpriteStruct.y
|
||||
sub eax, edi
|
||||
cdq
|
||||
xor eax, edx
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -89,8 +89,13 @@ init_spr_loop:
|
|||
|
||||
fallback_archetype:
|
||||
; If flags == 0, infer archetype from textureId:
|
||||
; Slot 13 = Enemy Guard, 14 = Medkit, 15 = Ammo
|
||||
; Slot 11 = Explosive Barrel, 13 = Guard, 14 = Medkit, 15 = Ammo
|
||||
mov edx, [ebx].SpriteStruct.textureId
|
||||
cmp edx, 11
|
||||
jne check_guard_slot
|
||||
mov [entityType + ecx*4], ENTITY_TYPE_BARREL
|
||||
jmp next_spr_slot
|
||||
check_guard_slot:
|
||||
cmp edx, 13
|
||||
jne check_medkit_slot
|
||||
mov [entityType + ecx*4], ENTITY_TYPE_GUARD
|
||||
|
|
@ -145,6 +150,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:
|
||||
|
|
@ -414,6 +422,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
|
||||
|
|
|
|||
|
|
@ -340,4 +340,3 @@ set_act_mode:
|
|||
proj_done:
|
||||
ret
|
||||
LoadProjectSettings ENDP
|
||||
|
||||
|
|
|
|||
|
|
@ -384,6 +384,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,19 @@
|
|||
|
||||
.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
|
||||
or eax, isGamePaused
|
||||
or eax, gameDialogActive
|
||||
ret
|
||||
IsGameplayBlocked ENDP
|
||||
|
||||
; ------------------------------------------------------------------------------
|
||||
; UpdatePlayer: Updates rotation, computes movement vector & handles collisions
|
||||
; ------------------------------------------------------------------------------
|
||||
|
|
@ -13,6 +26,13 @@ UpdatePlayer PROC uses ebx ecx edx esi edi
|
|||
LOCAL nextX:DWORD, nextY:DWORD
|
||||
LOCAL checkGridX:DWORD, checkGridY:DWORD
|
||||
|
||||
mov gameplayTickBlocked, 1
|
||||
cmp isGamePaused, 0
|
||||
jne update_player_done
|
||||
cmp gameDialogActive, 0
|
||||
jne update_player_done
|
||||
cmp isPlayerDead, 0
|
||||
jne update_player_done
|
||||
; Freeze player movement and inputs during level transitions
|
||||
cmp fadeState, 0
|
||||
jne update_player_done
|
||||
|
|
@ -37,6 +57,7 @@ scene_update_tick:
|
|||
call UpdateCutscene
|
||||
ret
|
||||
normal_player_update:
|
||||
mov gameplayTickBlocked, 0
|
||||
mov moveX, 0
|
||||
mov moveY, 0
|
||||
|
||||
|
|
@ -62,6 +83,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:
|
||||
|
|
@ -220,8 +243,9 @@ allow_portal_y:
|
|||
mov playerY, eax
|
||||
|
||||
update_player_done:
|
||||
cmp fadeState, 0
|
||||
jne @F
|
||||
call IsGameplayBlocked
|
||||
test eax, eax
|
||||
jnz @F
|
||||
call CheckPortalTriggers
|
||||
@@:
|
||||
ret
|
||||
|
|
|
|||
|
|
@ -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.
|
|
@ -24,6 +24,7 @@ static std::vector<uint8_t> Bytes(const fs::path& path) {
|
|||
#include "cutscene_editor_checks.inl"
|
||||
#include "lighting_editor_checks.inl"
|
||||
#include "viewport_editor_checks.inl"
|
||||
#include "stability_editor_checks.inl"
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
EditorApp app;
|
||||
|
|
@ -154,6 +155,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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -129,6 +129,8 @@ StateUnchanged ENDP
|
|||
|
||||
include tests/cutscene_engine_checks.inc
|
||||
include tests/lighting_engine_checks.inc
|
||||
include tests/stability_engine_checks.inc
|
||||
include tests/integration_engine_checks.inc
|
||||
|
||||
AuditMain PROC
|
||||
call InitMathTables
|
||||
|
|
@ -367,6 +369,7 @@ flat_depth_ok:
|
|||
|
||||
call RunLightingChecks
|
||||
call RunCutsceneChecks
|
||||
call RunStabilityChecks
|
||||
mov eax, resultCount
|
||||
inc eax
|
||||
shl eax, 2
|
||||
|
|
|
|||
85
tests/integration_engine_checks.inc
Normal file
85
tests/integration_engine_checks.inc
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
; Compatibility of stage-1 fixes with newer weapon/modal/lighting systems.
|
||||
.code
|
||||
RunIntegrationChecks PROC
|
||||
call ResetCombatFixture
|
||||
mov isGamePaused, 1
|
||||
mov keyForward, 1
|
||||
mov playerAttackCooldown, 4
|
||||
call UpdatePlayer
|
||||
call UpdateEntities
|
||||
call UpdateCombat
|
||||
CHECK playerX, 160 ; pause freezes player movement through production procedure
|
||||
CHECK [spriteList].SpriteStruct.x, 288 ; pause freezes guard AI through production procedure
|
||||
CHECK playerAttackCooldown, 4 ; pause freezes weapon cooldowns
|
||||
CHECK playerAmmo, 50 ; pause preserves ammunition
|
||||
mov isGamePaused, 0
|
||||
mov keyForward, 0
|
||||
call UpdatePlayer
|
||||
mov gameDialogActive, 1
|
||||
call UpdateEntities
|
||||
call UpdateCombat
|
||||
CHECK [spriteList].SpriteStruct.x, 288 ; modal dialogue freezes guards
|
||||
CHECK playerAmmo, 50 ; modal dialogue cannot fire
|
||||
mov gameDialogActive, 0
|
||||
mov gameMode, GAME_MODE_ADVENTURE
|
||||
call UpdateCombat
|
||||
CHECK entityHealth, 100 ; peaceful adventure mode cannot damage targets
|
||||
call ResetCombatFixture
|
||||
mov isPlayerDead, 1
|
||||
mov transitionCount, 1
|
||||
mov [transitionList].LevelTransition.triggerCellX, 2
|
||||
mov [transitionList].LevelTransition.triggerCellY, 2
|
||||
mov [transitionList].LevelTransition.fadeEffect, 1
|
||||
mov portalLatchCell, -1
|
||||
call UpdatePlayer
|
||||
CHECK fadeState, 0 ; dead player cannot activate a room transition
|
||||
|
||||
call ResetCombatFixture
|
||||
mov playerWeapon, WEAPON_KNIFE
|
||||
call UpdateCombat
|
||||
CHECK entityHealth, 100 ; knife cannot hit beyond its world range
|
||||
CHECK playerAmmo, 50 ; knife consumes no ammunition
|
||||
mov playerAttackCooldown, 0
|
||||
mov [spriteList].SpriteStruct.x, 208
|
||||
call UpdateCombat
|
||||
CHECK entityHealth, 70 ; knife retains its close-range damage after merge
|
||||
call ResetCombatFixture
|
||||
mov playerWeapon, WEAPON_SHOTGUN
|
||||
mov [spriteList].SpriteStruct.y, 184
|
||||
call UpdateCombat
|
||||
CHECK entityHealth, 30 ; shotgun retains wider world-space hitbox and damage
|
||||
CHECK playerAmmo, 48 ; shotgun consumes two rounds
|
||||
mov eax, [lightSources+14*sizeof LightSource].LightSource.flags
|
||||
and eax, LIGHT_FLAG_ACTIVE
|
||||
CHECK eax, 1 ; shotgun spawns muzzle light in reserved slot
|
||||
|
||||
call ResetCombatFixture
|
||||
mov [spriteList].SpriteStruct.textureId, 11
|
||||
mov [spriteList].SpriteStruct.flags, 06026400h
|
||||
call InitEntities
|
||||
call UpdateCombat
|
||||
CHECK [spriteList].SpriteStruct.textureId, 11 ; explicit Prop with barrel texture cannot explode
|
||||
mov [spriteList].SpriteStruct.flags, 0
|
||||
call InitEntities
|
||||
CHECK entityType, ENTITY_TYPE_BARREL ; legacy barrel archetype matches editor fallback
|
||||
|
||||
; Real explosion with a nearby guard: CDQ must not destroy sprite pointer.
|
||||
call ResetCombatFixture
|
||||
mov [spriteList].SpriteStruct.textureId, 11
|
||||
mov [spriteList].SpriteStruct.flags, ENTITY_TYPE_BARREL
|
||||
mov spriteCount, 2
|
||||
mov esi, OFFSET spriteList+sizeof SpriteStruct
|
||||
mov [esi].SpriteStruct.x, 352
|
||||
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 [spriteList].SpriteStruct.textureId, 0 ; explicit barrel still explodes on hitscan
|
||||
CHECK entityHealth[4], 40 ; blast damages nearby guard without corrupting pointer
|
||||
mov eax, [lightSources+15*sizeof LightSource].LightSource.flags
|
||||
and eax, LIGHT_FLAG_ACTIVE
|
||||
CHECK eax, 1 ; explosion spawns light in reserved slot
|
||||
ret
|
||||
RunIntegrationChecks ENDP
|
||||
71
tests/stability_editor_checks.inl
Normal file
71
tests/stability_editor_checks.inl
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
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");
|
||||
sprite.textureId = 11;
|
||||
sprite.flags = 0;
|
||||
Check(GetSpriteEntityType(sprite) == ENTITY_ARCHETYPE_BARREL, "zero flags retain new legacy barrel fallback");
|
||||
sprite.flags = 0x06026400;
|
||||
Check(GetSpriteEntityType(sprite) == ENTITY_ARCHETYPE_PROP, "explicit Prop overrides barrel artwork");
|
||||
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");
|
||||
}
|
||||
208
tests/stability_engine_checks.inc
Normal file
208
tests/stability_engine_checks.inc
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
; 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 isGamePaused, 0
|
||||
mov gameDialogActive, 0
|
||||
mov gameMode, GAME_MODE_ACTION
|
||||
mov enableWeapons, 1
|
||||
mov playerWeapon, WEAPON_PISTOL
|
||||
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
|
||||
call RunIntegrationChecks
|
||||
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() + (ROOT / "tests/lighting_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", "lighting_engine_checks.inc", "stability_engine_checks.inc", "integration_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