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)) {
|
if (ImGui::Combo("Архетип сущности", &archType, s_ArchetypeNames, 7)) {
|
||||||
PushUndo();
|
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 (archType == ENTITY_ARCHETYPE_GUARD) {
|
||||||
if (ImGui::SliderInt("Здоровье (HP)", &hp, 10, 500)) {
|
if (ImGui::SliderInt("Здоровье (HP)", &hp, 1, 255)) {
|
||||||
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("Скорость бега", &spd, 1, 8)) {
|
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)) {
|
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 карте");
|
ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), "● Отображается красным кругом на 2D карте");
|
||||||
} else if (archType == ENTITY_ARCHETYPE_MEDKIT) {
|
} else if (archType == ENTITY_ARCHETYPE_MEDKIT) {
|
||||||
|
|
|
||||||
|
|
@ -177,6 +177,7 @@ static const int g_EngineApiCount = sizeof(g_EngineApiCatalog) / sizeof(g_Engine
|
||||||
struct CodeEditorState {
|
struct CodeEditorState {
|
||||||
int currentFile = 0;
|
int currentFile = 0;
|
||||||
std::string fileContent;
|
std::string fileContent;
|
||||||
|
fs::path loadedPath;
|
||||||
std::vector<char> textBuffer;
|
std::vector<char> textBuffer;
|
||||||
bool isModified = false;
|
bool isModified = false;
|
||||||
std::string statusText = "Готов к редактированию кода MASM32";
|
std::string statusText = "Готов к редактированию кода MASM32";
|
||||||
|
|
@ -208,50 +209,64 @@ struct CodeEditorState {
|
||||||
|
|
||||||
void LoadFile(int fileIndex, const std::string& projectRoot) {
|
void LoadFile(int fileIndex, const std::string& projectRoot) {
|
||||||
if (fileIndex < 0 || fileIndex >= g_CodeFileCount) return;
|
if (fileIndex < 0 || fileIndex >= g_CodeFileCount) return;
|
||||||
currentFile = fileIndex;
|
if (isModified && !SaveFile(projectRoot)) return;
|
||||||
fs::path fullPath = fs::path(projectRoot) / g_ProjectCodeFiles[fileIndex].relativePath;
|
fs::path fullPath = fs::path(projectRoot) / g_ProjectCodeFiles[fileIndex].relativePath;
|
||||||
if (!fs::exists(fullPath)) {
|
if (!fs::exists(fullPath)) {
|
||||||
fullPath = fs::path(projectRoot).parent_path() / g_ProjectCodeFiles[fileIndex].relativePath;
|
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);
|
HANDLE hFile = CreateFileA(fullPath.string().c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||||
if (hFile != INVALID_HANDLE_VALUE) {
|
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;
|
|
||||||
statusText = "Не удалось открыть файл: " + fullPath.string();
|
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) {
|
bool SaveFile(const std::string& projectRoot) {
|
||||||
if (currentFile < 0 || currentFile >= g_CodeFileCount) return false;
|
if (currentFile < 0 || currentFile >= g_CodeFileCount || loadedPath.empty() || textBuffer.empty()) {
|
||||||
fs::path fullPath = fs::path(projectRoot) / g_ProjectCodeFiles[currentFile].relativePath;
|
statusText = "Сначала откройте исходный файл.";
|
||||||
if (!fs::exists(fullPath)) {
|
return false;
|
||||||
fullPath = fs::path(projectRoot).parent_path() / g_ProjectCodeFiles[currentFile].relativePath;
|
|
||||||
}
|
}
|
||||||
|
const auto fullPath = loadedPath;
|
||||||
HANDLE hFile = CreateFileA(fullPath.string().c_str(), GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
|
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) {
|
if (hFile == INVALID_HANDLE_VALUE) {
|
||||||
statusText = "Ошибка сохранения: доступ запрещен (" + fullPath.string() + ")";
|
statusText = "Ошибка сохранения: доступ запрещен (" + fullPath.string() + ")";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
DWORD len = static_cast<DWORD>(std::strlen(textBuffer.data()));
|
DWORD len = static_cast<DWORD>(std::strlen(textBuffer.data()));
|
||||||
DWORD written = 0;
|
DWORD written = 0;
|
||||||
WriteFile(hFile, textBuffer.data(), len, &written, nullptr);
|
bool ok = WriteFile(hFile, textBuffer.data(), len, &written, nullptr) && written == len;
|
||||||
CloseHandle(hFile);
|
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;
|
isModified = false;
|
||||||
statusText = "Файл сохранен: " + g_ProjectCodeFiles[currentFile].displayName;
|
statusText = "Файл сохранен: " + g_ProjectCodeFiles[currentFile].displayName;
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -261,7 +276,7 @@ struct CodeEditorState {
|
||||||
if (isBuilding) return;
|
if (isBuilding) return;
|
||||||
|
|
||||||
// Auto-save current file before building
|
// Auto-save current file before building
|
||||||
SaveFile(projectRoot);
|
if (!SaveFile(projectRoot)) return;
|
||||||
|
|
||||||
buildLog.clear();
|
buildLog.clear();
|
||||||
buildErrors.clear();
|
buildErrors.clear();
|
||||||
|
|
@ -303,9 +318,12 @@ struct CodeEditorState {
|
||||||
si.wShowWindow = SW_HIDE;
|
si.wShowWindow = SW_HIDE;
|
||||||
|
|
||||||
PROCESS_INFORMATION pi{};
|
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);
|
CloseHandle(pi.hThread);
|
||||||
hBuildProcess = pi.hProcess;
|
hBuildProcess = pi.hProcess;
|
||||||
isBuilding = true;
|
isBuilding = true;
|
||||||
|
|
@ -314,6 +332,8 @@ struct CodeEditorState {
|
||||||
buildLog = "[ERROR] Не удалось запустить процесс сборки.\n";
|
buildLog = "[ERROR] Не удалось запустить процесс сборки.\n";
|
||||||
hasBuildResult = true;
|
hasBuildResult = true;
|
||||||
buildExitCode = 1;
|
buildExitCode = 1;
|
||||||
|
CloseHandle(hReadPipe);
|
||||||
|
hReadPipe = nullptr;
|
||||||
}
|
}
|
||||||
CloseHandle(hWritePipe);
|
CloseHandle(hWritePipe);
|
||||||
}
|
}
|
||||||
|
|
@ -404,9 +424,9 @@ struct CodeEditorState {
|
||||||
static CodeEditorState g_CodeEditor;
|
static CodeEditorState g_CodeEditor;
|
||||||
|
|
||||||
void EditorApp::RenderCodeEditorWindow() {
|
void EditorApp::RenderCodeEditorWindow() {
|
||||||
if (!showCodeEditorModal) return;
|
// Drain compiler output even when the IDE is hidden; a full pipe blocks ml/link.
|
||||||
|
|
||||||
g_CodeEditor.PollBuild();
|
g_CodeEditor.PollBuild();
|
||||||
|
if (!showCodeEditorModal) return;
|
||||||
|
|
||||||
ImGui::SetNextWindowSize(ImVec2(1020, 680), ImGuiCond_FirstUseEver);
|
ImGui::SetNextWindowSize(ImVec2(1020, 680), ImGuiCond_FirstUseEver);
|
||||||
if (!ImGui::Begin("Редактор кода MASM32 & Игровых скриптов (F7)", &showCodeEditorModal, ImGuiWindowFlags_MenuBar)) {
|
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) {
|
for (int i = 0; i < g_CodeFileCount; ++i) {
|
||||||
bool isSelected = (g_CodeEditor.currentFile == i);
|
bool isSelected = (g_CodeEditor.currentFile == i);
|
||||||
if (ImGui::Selectable(g_ProjectCodeFiles[i].displayName.c_str(), isSelected)) {
|
if (ImGui::Selectable(g_ProjectCodeFiles[i].displayName.c_str(), isSelected)) {
|
||||||
if (g_CodeEditor.isModified) {
|
|
||||||
g_CodeEditor.SaveFile(root);
|
|
||||||
}
|
|
||||||
g_CodeEditor.LoadFile(i, 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) {
|
for (int f = 0; f < g_CodeFileCount; ++f) {
|
||||||
if (err.file.find(g_ProjectCodeFiles[f].relativePath) != std::string::npos ||
|
if (err.file.find(g_ProjectCodeFiles[f].relativePath) != std::string::npos ||
|
||||||
g_ProjectCodeFiles[f].relativePath.find(err.file) != 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);
|
g_CodeEditor.LoadFile(f, root);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -105,7 +105,7 @@ struct SpriteEntry {
|
||||||
|
|
||||||
inline uint8_t GetSpriteEntityType(const SpriteEntry& s) {
|
inline uint8_t GetSpriteEntityType(const SpriteEntry& s) {
|
||||||
uint8_t t = static_cast<uint8_t>(s.flags & 0xFF);
|
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 == 11) return ENTITY_ARCHETYPE_BARREL;
|
||||||
if (s.textureId == 13) return ENTITY_ARCHETYPE_GUARD;
|
if (s.textureId == 13) return ENTITY_ARCHETYPE_GUARD;
|
||||||
if (s.textureId == 14) return ENTITY_ARCHETYPE_MEDKIT;
|
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);
|
uint8_t agg = static_cast<uint8_t>((s.flags >> 24) & 0xFF);
|
||||||
return (agg == 0) ? 6 : agg;
|
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) |
|
s.flags = static_cast<uint32_t>(type) |
|
||||||
(static_cast<uint32_t>(health) << 8) |
|
(static_cast<uint32_t>(health) << 8) |
|
||||||
(static_cast<uint32_t>(speed) << 16) |
|
(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
|
ShowGameToast PROTO :DWORD, :DWORD, :DWORD
|
||||||
Game_OnEntityKilled PROTO :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
|
; 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 i:DWORD, closestIdx:DWORD, closestDist:DWORD
|
||||||
LOCAL sprX:DWORD, sprY:DWORD
|
LOCAL sprX:DWORD, sprY:DWORD
|
||||||
LOCAL transX:DWORD, transY:DWORD
|
LOCAL transX:DWORD, transY:DWORD
|
||||||
LOCAL sprScreenX:DWORD, wallDepth:DWORD
|
LOCAL targetHalfWidth:DWORD, wallDepth:DWORD
|
||||||
LOCAL weaponDamage:DWORD, maxHitDist:DWORD, hitTolerance:DWORD
|
LOCAL weaponDamage:DWORD, maxHitDist:DWORD, hitHalfWidth:DWORD
|
||||||
|
|
||||||
|
call IsGameplayBlocked
|
||||||
|
test eax, eax
|
||||||
|
jnz combat_done
|
||||||
|
|
||||||
; 1. Decrement cooldowns
|
; 1. Decrement cooldowns
|
||||||
cmp playerAttackCooldown, 0
|
cmp playerAttackCooldown, 0
|
||||||
|
|
@ -58,7 +186,7 @@ UpdateCombat PROC uses ebx ecx edx esi edi
|
||||||
; Set weapon parameters based on playerWeapon
|
; Set weapon parameters based on playerWeapon
|
||||||
mov weaponDamage, 35
|
mov weaponDamage, 35
|
||||||
mov maxHitDist, 999999
|
mov maxHitDist, 999999
|
||||||
mov hitTolerance, 22
|
mov hitHalfWidth, 16
|
||||||
|
|
||||||
cmp playerWeapon, WEAPON_KNIFE
|
cmp playerWeapon, WEAPON_KNIFE
|
||||||
je setup_knife
|
je setup_knife
|
||||||
|
|
@ -70,32 +198,32 @@ setup_knife:
|
||||||
; Knife: no ammo needed, close range (within 64 units = 1 cell), fast cooldown
|
; Knife: no ammo needed, close range (within 64 units = 1 cell), fast cooldown
|
||||||
mov weaponDamage, 30
|
mov weaponDamage, 30
|
||||||
mov maxHitDist, 64
|
mov maxHitDist, 64
|
||||||
mov hitTolerance, 32
|
mov hitHalfWidth, 24
|
||||||
mov playerAttackCooldown, 8
|
mov playerAttackCooldown, 8
|
||||||
mov playerFlashTimer, 3
|
mov playerFlashTimer, 3
|
||||||
jmp fire_hitscan
|
jmp fire_hitscan
|
||||||
|
|
||||||
setup_shotgun:
|
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
|
cmp playerAmmo, 2
|
||||||
jl combat_dry_fire
|
jl combat_dry_fire
|
||||||
sub playerAmmo, 2
|
sub playerAmmo, 2
|
||||||
mov weaponDamage, 70
|
mov weaponDamage, 70
|
||||||
mov maxHitDist, 999999
|
mov maxHitDist, 999999
|
||||||
mov hitTolerance, 40
|
mov hitHalfWidth, 32
|
||||||
mov playerAttackCooldown, 22
|
mov playerAttackCooldown, 22
|
||||||
mov playerFlashTimer, 7
|
mov playerFlashTimer, 7
|
||||||
invoke SpawnMuzzleFlashLight, playerX, playerY
|
invoke SpawnMuzzleFlashLight, playerX, playerY
|
||||||
jmp fire_hitscan
|
jmp fire_hitscan
|
||||||
|
|
||||||
setup_pistol:
|
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
|
cmp playerAmmo, 1
|
||||||
jl combat_dry_fire
|
jl combat_dry_fire
|
||||||
dec playerAmmo
|
dec playerAmmo
|
||||||
mov weaponDamage, 35
|
mov weaponDamage, 35
|
||||||
mov maxHitDist, 999999
|
mov maxHitDist, 999999
|
||||||
mov hitTolerance, 22
|
mov hitHalfWidth, 16
|
||||||
mov playerAttackCooldown, 12
|
mov playerAttackCooldown, 12
|
||||||
mov playerFlashTimer, 5
|
mov playerFlashTimer, 5
|
||||||
invoke SpawnMuzzleFlashLight, playerX, playerY
|
invoke SpawnMuzzleFlashLight, playerX, playerY
|
||||||
|
|
@ -111,8 +239,8 @@ fire_hitscan:
|
||||||
mov edx, [tableSin + eax*4]
|
mov edx, [tableSin + eax*4]
|
||||||
mov camSin, edx
|
mov camSin, edx
|
||||||
|
|
||||||
; Center wall depth from depthBuffer[160]
|
; Query current geometry, never the previous rendered frame.
|
||||||
mov eax, [depthBuffer + SCREEN_HALF_W * 4]
|
call TraceShotWall
|
||||||
mov wallDepth, eax
|
mov wallDepth, eax
|
||||||
|
|
||||||
mov closestIdx, -1
|
mov closestIdx, -1
|
||||||
|
|
@ -133,14 +261,6 @@ hitscan_loop:
|
||||||
cmp eax, ENTITY_TYPE_BARREL
|
cmp eax, ENTITY_TYPE_BARREL
|
||||||
je hitscan_target_valid
|
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
|
jmp next_hitscan_target
|
||||||
|
|
||||||
hitscan_target_valid:
|
hitscan_target_valid:
|
||||||
|
|
@ -185,8 +305,9 @@ hitscan_target_valid:
|
||||||
cmp eax, maxHitDist
|
cmp eax, maxHitDist
|
||||||
jg next_hitscan_target
|
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
|
mov eax, transY
|
||||||
|
shl eax, 8
|
||||||
cmp eax, wallDepth
|
cmp eax, wallDepth
|
||||||
jge next_hitscan_target
|
jge next_hitscan_target
|
||||||
|
|
||||||
|
|
@ -201,20 +322,21 @@ hitscan_target_valid:
|
||||||
add ecx, eax
|
add ecx, eax
|
||||||
mov transX, ecx
|
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
|
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
|
cdq
|
||||||
xor eax, edx
|
xor eax, edx
|
||||||
sub eax, edx ; abs(sprScreenX - 160)
|
sub eax, edx
|
||||||
cmp eax, hitTolerance
|
cmp eax, targetHalfWidth
|
||||||
jg next_hitscan_target
|
jg next_hitscan_target
|
||||||
|
|
||||||
; Check if closer than previous target
|
; Check if closer than previous target
|
||||||
|
|
@ -239,12 +361,6 @@ hitscan_eval:
|
||||||
; Check if hit an explosive barrel!
|
; Check if hit an explosive barrel!
|
||||||
cmp eax, ENTITY_TYPE_BARREL
|
cmp eax, ENTITY_TYPE_BARREL
|
||||||
je explode_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!
|
; Hit an enemy guard!
|
||||||
mov eax, [entityHealth + ecx*4]
|
mov eax, [entityHealth + ecx*4]
|
||||||
sub eax, weaponDamage
|
sub eax, weaponDamage
|
||||||
|
|
@ -308,11 +424,12 @@ aoe_loop:
|
||||||
cmp [entityState + ecx*4], ENTITY_STATE_DEAD
|
cmp [entityState + ecx*4], ENTITY_STATE_DEAD
|
||||||
je next_aoe
|
je next_aoe
|
||||||
|
|
||||||
mov edx, i
|
; EDX is volatile scratch for CDQ below, not a safe sprite pointer.
|
||||||
imul edx, sizeof SpriteStruct
|
mov ebx, i
|
||||||
add edx, OFFSET spriteList
|
imul ebx, sizeof SpriteStruct
|
||||||
|
add ebx, OFFSET spriteList
|
||||||
|
|
||||||
mov eax, [edx].SpriteStruct.x
|
mov eax, [ebx].SpriteStruct.x
|
||||||
sub eax, esi
|
sub eax, esi
|
||||||
cdq
|
cdq
|
||||||
xor eax, edx
|
xor eax, edx
|
||||||
|
|
@ -320,7 +437,7 @@ aoe_loop:
|
||||||
cmp eax, 128
|
cmp eax, 128
|
||||||
jg next_aoe
|
jg next_aoe
|
||||||
|
|
||||||
mov eax, [edx].SpriteStruct.y
|
mov eax, [ebx].SpriteStruct.y
|
||||||
sub eax, edi
|
sub eax, edi
|
||||||
cdq
|
cdq
|
||||||
xor eax, edx
|
xor eax, edx
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,9 @@ hEngineInstance dd 0
|
||||||
isRunning dd 1
|
isRunning dd 1
|
||||||
|
|
||||||
; High Precision Timing
|
; 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)
|
targetFrameTimeMs dd 16 ; ~60 FPS (16.6 ms)
|
||||||
lastTickCount dd 0
|
lastTickCount dd 0
|
||||||
fpsCounter dd 0
|
fpsCounter dd 0
|
||||||
|
|
|
||||||
|
|
@ -89,8 +89,13 @@ init_spr_loop:
|
||||||
|
|
||||||
fallback_archetype:
|
fallback_archetype:
|
||||||
; If flags == 0, infer archetype from textureId:
|
; 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
|
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
|
cmp edx, 13
|
||||||
jne check_medkit_slot
|
jne check_medkit_slot
|
||||||
mov [entityType + ecx*4], ENTITY_TYPE_GUARD
|
mov [entityType + ecx*4], ENTITY_TYPE_GUARD
|
||||||
|
|
@ -145,6 +150,9 @@ UpdateEntities PROC uses ebx ecx edx esi edi
|
||||||
ret
|
ret
|
||||||
|
|
||||||
entities_active:
|
entities_active:
|
||||||
|
call IsGameplayBlocked
|
||||||
|
test eax, eax
|
||||||
|
jnz entities_done
|
||||||
mov i, 0
|
mov i, 0
|
||||||
|
|
||||||
entity_loop:
|
entity_loop:
|
||||||
|
|
@ -414,6 +422,8 @@ check_guard_attack:
|
||||||
test eax, eax
|
test eax, eax
|
||||||
jnz next_entity
|
jnz next_entity
|
||||||
mov isPlayerDead, 1
|
mov isPlayerDead, 1
|
||||||
|
; Nothing else may collect items or attack after a lethal hit this tick.
|
||||||
|
jmp entities_done
|
||||||
|
|
||||||
next_entity:
|
next_entity:
|
||||||
inc i
|
inc i
|
||||||
|
|
|
||||||
|
|
@ -340,4 +340,3 @@ set_act_mode:
|
||||||
proj_done:
|
proj_done:
|
||||||
ret
|
ret
|
||||||
LoadProjectSettings ENDP
|
LoadProjectSettings ENDP
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -384,6 +384,9 @@ SwitchSublevel PROC uses esi edi, pszNextMap:DWORD, spawnX:DWORD, spawnY:DWORD,
|
||||||
cmp fadeState, 0
|
cmp fadeState, 0
|
||||||
jne switch_busy
|
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
|
mov esi, pszNextMap
|
||||||
lea edi, pendingMapPath
|
lea edi, pendingMapPath
|
||||||
mov ecx, 32
|
mov ecx, 32
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,19 @@
|
||||||
|
|
||||||
.code
|
.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
|
; 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 nextX:DWORD, nextY:DWORD
|
||||||
LOCAL checkGridX:DWORD, checkGridY: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
|
; Freeze player movement and inputs during level transitions
|
||||||
cmp fadeState, 0
|
cmp fadeState, 0
|
||||||
jne update_player_done
|
jne update_player_done
|
||||||
|
|
@ -37,6 +57,7 @@ scene_update_tick:
|
||||||
call UpdateCutscene
|
call UpdateCutscene
|
||||||
ret
|
ret
|
||||||
normal_player_update:
|
normal_player_update:
|
||||||
|
mov gameplayTickBlocked, 0
|
||||||
mov moveX, 0
|
mov moveX, 0
|
||||||
mov moveY, 0
|
mov moveY, 0
|
||||||
|
|
||||||
|
|
@ -62,6 +83,8 @@ check_action:
|
||||||
call InteractDoor
|
call InteractDoor
|
||||||
cmp fadeState, 0
|
cmp fadeState, 0
|
||||||
jne update_player_done
|
jne update_player_done
|
||||||
|
cmp gameplayTickBlocked, 0
|
||||||
|
jne update_player_done
|
||||||
|
|
||||||
; 2. Determine Movement Speed
|
; 2. Determine Movement Speed
|
||||||
check_movement:
|
check_movement:
|
||||||
|
|
@ -220,8 +243,9 @@ allow_portal_y:
|
||||||
mov playerY, eax
|
mov playerY, eax
|
||||||
|
|
||||||
update_player_done:
|
update_player_done:
|
||||||
cmp fadeState, 0
|
call IsGameplayBlocked
|
||||||
jne @F
|
test eax, eax
|
||||||
|
jnz @F
|
||||||
call CheckPortalTriggers
|
call CheckPortalTriggers
|
||||||
@@:
|
@@:
|
||||||
ret
|
ret
|
||||||
|
|
|
||||||
|
|
@ -90,9 +90,13 @@ DrawRect ENDP
|
||||||
; ------------------------------------------------------------------------------
|
; ------------------------------------------------------------------------------
|
||||||
; DrawRectOutline: Draws a 1-pixel rectangle outline
|
; 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
|
LOCAL x2:DWORD, y2:DWORD
|
||||||
|
|
||||||
|
cmp w, 0
|
||||||
|
jle outline_done
|
||||||
|
cmp h, 0
|
||||||
|
jle outline_done
|
||||||
mov eax, x
|
mov eax, x
|
||||||
add eax, w
|
add eax, w
|
||||||
dec eax
|
dec eax
|
||||||
|
|
@ -104,26 +108,28 @@ DrawRectOutline PROC x:DWORD, y:DWORD, w:DWORD, h:DWORD, color:DWORD
|
||||||
mov y2, eax
|
mov y2, eax
|
||||||
|
|
||||||
; Top & Bottom horizontal lines
|
; 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
|
jg @F
|
||||||
invoke DrawPixel, ecx, y, color
|
invoke DrawPixel, ebx, y, color
|
||||||
invoke DrawPixel, ecx, y2, color
|
invoke DrawPixel, ebx, y2, color
|
||||||
inc ecx
|
inc ebx
|
||||||
jmp @B
|
jmp @B
|
||||||
@@:
|
@@:
|
||||||
|
|
||||||
; Left & Right vertical lines
|
; Left & Right vertical lines
|
||||||
mov edx, y
|
mov ebx, y
|
||||||
@@:
|
@@:
|
||||||
cmp edx, y2
|
cmp ebx, y2
|
||||||
jg @F
|
jg @F
|
||||||
invoke DrawPixel, x, edx, color
|
invoke DrawPixel, x, ebx, color
|
||||||
invoke DrawPixel, x2, edx, color
|
invoke DrawPixel, x2, ebx, color
|
||||||
inc edx
|
inc ebx
|
||||||
jmp @B
|
jmp @B
|
||||||
@@:
|
@@:
|
||||||
|
outline_done:
|
||||||
ret
|
ret
|
||||||
DrawRectOutline ENDP
|
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 "cutscene_editor_checks.inl"
|
||||||
#include "lighting_editor_checks.inl"
|
#include "lighting_editor_checks.inl"
|
||||||
#include "viewport_editor_checks.inl"
|
#include "viewport_editor_checks.inl"
|
||||||
|
#include "stability_editor_checks.inl"
|
||||||
|
|
||||||
int main(int argc, char** argv) {
|
int main(int argc, char** argv) {
|
||||||
EditorApp app;
|
EditorApp app;
|
||||||
|
|
@ -154,6 +155,7 @@ int main(int argc, char** argv) {
|
||||||
app.CleanupPlaytest();
|
app.CleanupPlaytest();
|
||||||
}
|
}
|
||||||
if(argc>2) RunViewportChecks(app,argv[2]);
|
if(argc>2) RunViewportChecks(app,argv[2]);
|
||||||
|
RunStabilityEditorChecks();
|
||||||
std::cout << "Editor: " << checks - failures << '/' << checks << " passed\n";
|
std::cout << "Editor: " << checks - failures << '/' << checks << " passed\n";
|
||||||
return failures ? 1 : 0;
|
return failures ? 1 : 0;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -129,6 +129,8 @@ StateUnchanged ENDP
|
||||||
|
|
||||||
include tests/cutscene_engine_checks.inc
|
include tests/cutscene_engine_checks.inc
|
||||||
include tests/lighting_engine_checks.inc
|
include tests/lighting_engine_checks.inc
|
||||||
|
include tests/stability_engine_checks.inc
|
||||||
|
include tests/integration_engine_checks.inc
|
||||||
|
|
||||||
AuditMain PROC
|
AuditMain PROC
|
||||||
call InitMathTables
|
call InitMathTables
|
||||||
|
|
@ -367,6 +369,7 @@ flat_depth_ok:
|
||||||
|
|
||||||
call RunLightingChecks
|
call RunLightingChecks
|
||||||
call RunCutsceneChecks
|
call RunCutsceneChecks
|
||||||
|
call RunStabilityChecks
|
||||||
mov eax, resultCount
|
mov eax, resultCount
|
||||||
inc eax
|
inc eax
|
||||||
shl eax, 2
|
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(
|
(work / "project_paths.inc").write_text(
|
||||||
f'.data\nprojectMapPath db "{ROOT / "assets/map.dat"}",0\n'
|
f'.data\nprojectMapPath db "{ROOT / "assets/map.dat"}",0\n'
|
||||||
f'projectPackPath db "{ROOT / "assets/textures/texturepack.bin"}",0\n.code\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"))
|
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)
|
ml = next((p for p in masm_candidates if p.is_file()), None)
|
||||||
if not ml:
|
if not ml:
|
||||||
|
|
@ -148,7 +148,9 @@ def main():
|
||||||
f"/OUT:{work / 'engine_tests.exe'}", str(work / "engine_tests.obj"),
|
f"/OUT:{work / 'engine_tests.exe'}", str(work / "engine_tests.obj"),
|
||||||
"kernel32.lib", "user32.lib", "gdi32.lib", "winmm.lib"], work)
|
"kernel32.lib", "user32.lib", "gdi32.lib", "winmm.lib"], work)
|
||||||
run([str(work / "engine_tests.exe"), "pace map.dat"], 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)
|
names = re.findall(r"^\s+CHECK .*?; (.+)$", source, re.M)
|
||||||
raw = (work / "engine_results.bin").read_bytes()
|
raw = (work / "engine_results.bin").read_bytes()
|
||||||
values = struct.unpack(f"<{len(raw)//4}I", raw)
|
values = struct.unpack(f"<{len(raw)//4}I", raw)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue