Fix two issues with the battleLogMessage feature:
1. Translation not working: string registration now happens in
CSpellHandler::loadFromJson (before translations are loaded)
instead of in Timed::serializeJsonUnitEffect (which runs in
afterLoadFinalization, after translations are already loaded).
The effect now only generates the text ID without registering.
2. Singular/plural support: battleLogMessage is now an object with
"singular" and "plural" fields for grammatically correct messages
based on stack size.
Example: { "singular": "The %s is petrified.",
"plural": "The %s are petrified." }
Values starting with @ reference an existing text ID instead of
registering a new string. Core spells now use @core.genrltxt.XXX
to reuse the original H3 translations directly.
Timed spell effects (core:timed) now support an optional
"battleLogMessage" field displayed in the battle log when the effect
is applied. The text should include a %s placeholder for the affected
creature's name.
The field contains the English text directly (same pattern as spell
name/description). A text ID for translation export is auto-generated
at load time as: spell.<scope>.<identifier>.<effectName>.battleLogMessage
String registration happens in the effect itself during deserialization
(Timed::serializeJsonUnitEffect), using spell scope, identifier, and
effect name passed down through the Effects loading chain.
This replaces the previously hardcoded battle log messages for stone
gaze, paralyze, poison, disease, and bind, which are now converted
from the old effects format to the new battleEffects format with
configurable messages. The age ability retains its hardcoded message
because it computes health loss at runtime.
Resolves: https://github.com/vcmi-mods/horn-of-the-abyss/issues/551
Fixes cases where game would try to access file on real filesystem for
VCMI campaigns files that are actually located inside .zip archive.
Made search for other usages of last_write_time access, don't see any
other similar cases left
Normalize quoted local relative include directives to remove redundant
path segments while preserving relative includes.
This applies the normalizer across the tree and updates 108 includes in
64 files, including cases where paths inside lib/* redundantly used
../../lib/... and now correctly use ../... .
These paths compiled before because include resolution normalized them,
but they were longer and harder to audit.
Many quoted local includes had an incorrect ../ depth and resolved to
non-existent files from the including file's directory.
This was easy to miss because normal target include directories and PCH
usage masked some failures, and several stale paths lived in files that
are only compiled in optional test configurations. As a result, the
problem mostly surfaced in stricter or broader fresh builds.
Audit all C++ and header local includes, keep them relative, and adjust
paths so each include resolves to an existing in-tree header. For
headers that were renamed or moved, update includes to their current
relative location instead of switching to include-root form.
A few legacy ERM tests also used dynamic_ptr_cast at call sites where we
had to replace stale headers. The helper/header path they relied on is
no longer present after 81af66d35b, so
those downcasts are now explicit dynamic_cast calls with the same intent.
ASan/LSan fuzz runs were reporting large indirect leaks at exit. The
stack traces touched many RMG modificators (TreasurePlacer, RiverPlacer,
WaterAdopter, ObjectManager), which looked like unrelated leaks at first.
The common root cause is ownership: each Zone owns modificators, while
QuestArtifactPlacer kept neighboring zones in
std::vector<std::shared_ptr<Zone>>. That creates a strong reference cycle
between zones and their modificators, so clearing map zones cannot release
the whole graph and LeakSanitizer reports it as leaked memory.
This was verified by running the same fuzzer command with leak detection
enabled before and after changing only this edge. Before the change, LSAN
consistently reported multi-megabyte indirect leaks; after the change, the
report disappears while fuzzing behavior stays the same.
Use weak_ptr for cross-zone references in QuestArtifactPlacer and lock the
references when iterating. This keeps behavior intact while breaking the
cycle so normal cleanup can reclaim zone state.
Selecting certain objects in the editor could abort both on a freshly generated
random map and after saving and reloading a map with random artifacts. The
runtime error was std::out_of_range from vector::_M_range_check, with __n shown
as 18446744073709551615, and the stack passed through
Inspector::updateProperties(CGArtifact*), CGArtifact::getArtifactInstance, and
CMap::getArtifactInstance.
The immediate problem was an unset ArtifactInstanceID (-1) on artifact-like
objects. Inspector asked for artifact instance unconditionally, and
CGArtifact::getArtifactInstance forwarded the invalid id into map storage.
The change handles this at both levels involved in the crash path. Inspector now
requests artifact instance only for spell scrolls, and CGArtifact now returns
nullptr when the stored id is unset.
A regression test in vcmitest now constructs EditorCallback + CMap + CGArtifact
with random artifact id and default stored instance, then checks that
getArtifactInstance does not throw and returns nullptr. It fails before the fix
with the same out_of_range path and passes after it.
On Unix, development mode only activated when the working directory
contained config and Mods plus one of the game binaries. A build that
only produced vcmieditor failed that check, so the editor looked for
installed data paths and aborted on config/filesystem.json.
Treat vcmieditor as a valid development binary so editor-only builds use
the local build tree data layout and start correctly.
Fix the following crash:
$ ./out/build/linux-gcc-release/bin/vcmieditor
Starting map editor of 'VCMI 1.8.0.'
...
Disaster happened.
Reason: Resource with name CONFIG/FILESYSTEM and type JSON wasn't found.