Recovers the game's event-posting API so the engine can post events and the
compare harness can see them. Until now the owner's event list was invisible to
every layer: B3's replace-mode oracle failed by exactly one item across 40,300
(an unposted EVENT_RESEARCH_OVERBUDGET) while its compare read clean, and B2's
clean compare bounds the economy fields only.
New pure module src/game/events:
* EventStorage / TurnEvents / PlayerEvent -- the list is bucketed by TURN, not
flat, which the save-editor struct notes had wrong.
* EventStorage::Post reproducing the original's rules, including the four that
change save bytes: the FLT_MAX (not infinity) default position; action 0 with
no subject and no position storing as 2; per-bucket dedup that compares
message/image/location/position/action but NOT summary; and EvNxID starting
at 0 and being promoted to 1 on the first post.
* PruneOldTurns reproduced with its off-by-one: of a leading run of buckets
older than turn-50 it erases n-1, so one stale bucket always survives. The
survivor is serialized, so correcting it would diverge.
* research_events: the five events the research path raises, their EvImg
identifiers and string-table keys, and the 0.8 completion split evaluated
against (double)0.8f rather than the decimal 0.8.
Localized text is deliberately absent: only the EVENTSUM_/EVENTMSG_ keys are
here and the text resolves through a caller-supplied lookup, as the game does.
The four event offsets the B3 hook carried as local literals now come from the
generated header; they are read off instructions rather than inferred from the
save schema.
tests/game_events: 112 checks including a replay of the event list
turn3-state.sav actually holds. ctest 31/31 -> 32/32.
docs/E-events.md carries the proposed region and Coverage wording for the next
B3 recapture.
88 lines
3.6 KiB
C++
88 lines
3.6 KiB
C++
#include "game/events/research_events.h"
|
|
|
|
namespace sots::events {
|
|
|
|
bool ResearchCompletedOnBudget(float progressRatio) {
|
|
// The original widens the float ratio and compares it against the double nearest 0.8f.
|
|
return static_cast<double>(progressRatio) >= kResearchOnBudgetRatio;
|
|
}
|
|
|
|
std::string FormatEventText(std::string_view fmt, std::string_view arg, std::size_t cap) {
|
|
std::string out;
|
|
out.reserve(fmt.size() + arg.size());
|
|
bool substituted = false;
|
|
for (std::size_t i = 0; i < fmt.size(); ++i) {
|
|
if (fmt[i] == '%' && i + 1 < fmt.size()) {
|
|
if (fmt[i + 1] == 's' && !substituted) {
|
|
out.append(arg.begin(), arg.end());
|
|
substituted = true;
|
|
++i;
|
|
continue;
|
|
}
|
|
if (fmt[i + 1] == '%') {
|
|
out.push_back('%');
|
|
++i;
|
|
continue;
|
|
}
|
|
}
|
|
out.push_back(fmt[i]);
|
|
}
|
|
if (cap != 0 && out.size() > cap) out.resize(cap);
|
|
return out;
|
|
}
|
|
|
|
namespace {
|
|
|
|
int PostPair(EventStorage& log, const EventText& text, const EventTextKeys& keys,
|
|
std::string_view arg, std::size_t cap, int turn, std::string_view img, int action) {
|
|
std::string summary = FormatEventText(text(keys.summaryKey), arg, cap);
|
|
std::string message = FormatEventText(text(keys.messageKey), arg, cap);
|
|
return log.PostGlobal(std::move(summary), std::move(message), turn, img, action);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
int PostResearchOverbudget(EventStorage& log, const EventText& text, std::string_view techName,
|
|
int turn) {
|
|
// Formatted into a std::string by the original, so no length cap.
|
|
return PostPair(log, text, kTextResearchOverbudget, techName, 0, turn, kImgResearchOverbudget,
|
|
1);
|
|
}
|
|
|
|
int PostResearchCompleted(EventStorage& log, const EventText& text, std::string_view techName,
|
|
float progressRatio, int turn) {
|
|
const bool onBudget = ResearchCompletedOnBudget(progressRatio);
|
|
const EventTextKeys& keys = onBudget ? kTextResearchComplete : kTextResearchUnderbudget;
|
|
const std::string_view img = onBudget ? kImgResearchComplete : kImgResearchUnderbudget;
|
|
|
|
// Only the message goes through the 256-byte buffer; the summary is taken verbatim from
|
|
// its slot without formatting.
|
|
std::string summary(text(keys.summaryKey));
|
|
std::string message = FormatEventText(text(keys.messageKey), techName, kCompletionMessageCap);
|
|
return log.PostGlobal(std::move(summary), std::move(message), turn, img, 1);
|
|
}
|
|
|
|
int PostTemperance(EventStorage& log, const EventText& text, int turn) {
|
|
// action 0 with no subject and no position -> EventStorage::Post stores 2.
|
|
return PostPair(log, text, kTextTemperance, {}, 0, turn, kImgTemperance, 0);
|
|
}
|
|
|
|
int PostTechsUnlocked(EventStorage& log, const EventText& text,
|
|
const std::vector<std::string>& techNames, std::string_view separator,
|
|
int turn) {
|
|
std::string summary(text(kTextTechsUnlocked.summaryKey));
|
|
std::string message(text(kTextTechsUnlocked.messageKey));
|
|
for (const std::string& name : techNames) {
|
|
message.append(separator.begin(), separator.end());
|
|
message.append(name);
|
|
}
|
|
return log.PostGlobal(std::move(summary), std::move(message), turn, kImgTechsUnlocked, 1);
|
|
}
|
|
|
|
int PostNoResearch(EventStorage& log, const EventText& text, int turn) {
|
|
std::string summary(text(kTextNoResearch.summaryKey));
|
|
std::string message(text(kTextNoResearch.messageKey));
|
|
return log.PostGlobal(std::move(summary), std::move(message), turn, kImgNoResearch, 1);
|
|
}
|
|
|
|
} // namespace sots::events
|