Add custom message lists support

Closes #130
See #200
This commit is contained in:
Alexander Batalov 2022-11-10 18:07:23 +03:00
parent 6c03e4e293
commit fe9ba9171e
18 changed files with 337 additions and 0 deletions

View File

@ -2022,6 +2022,8 @@ int combatInit()
return -1;
}
messageListRepositorySetStandardMessageList(STANDARD_MESSAGE_LIST_COMBAT, &gCombatMessageList);
// SFALL
criticalsInit();
burstModInit();
@ -2061,6 +2063,7 @@ void combatReset()
// 0x420E14
void combatExit()
{
messageListRepositorySetStandardMessageList(STANDARD_MESSAGE_LIST_COMBAT, nullptr);
messageListFree(&gCombatMessageList);
// SFALL

View File

@ -3433,6 +3433,8 @@ static int aiMessageListInit()
messageListFilterBadwords(&gCombatAiMessageList);
}
messageListRepositorySetStandardMessageList(STANDARD_MESSAGE_LIST_COMBAT_AI, &gCombatAiMessageList);
return 0;
}
@ -3441,6 +3443,7 @@ static int aiMessageListInit()
// 0x42BBD8
static int aiMessageListFree()
{
messageListRepositorySetStandardMessageList(STANDARD_MESSAGE_LIST_COMBAT_AI, nullptr);
if (!messageListFree(&gCombatAiMessageList)) {
return -1;
}

View File

@ -178,6 +178,8 @@ int critterInit()
return -1;
}
messageListRepositorySetStandardMessageList(STANDARD_MESSAGE_LIST_SCRNAME, &gCritterMessageList);
return 0;
}
@ -193,6 +195,7 @@ void critterReset()
// 0x42D004
void critterExit()
{
messageListRepositorySetStandardMessageList(STANDARD_MESSAGE_LIST_SCRNAME, nullptr);
messageListFree(&gCritterMessageList);
}

View File

@ -151,6 +151,10 @@ int gameInitWithOptions(const char* windowTitle, bool isMapper, int font, int a4
return -1;
}
// Message list repository is considered a specialized file manager, so
// it should be initialized early in the process.
messageListRepositoryInit();
runElectronicRegistration();
programWindowSetTitle(windowTitle);
_initWindow(1, a4);
@ -358,6 +362,8 @@ int gameInitWithOptions(const char* windowTitle, bool isMapper, int font, int a4
return -1;
}
messageListRepositorySetStandardMessageList(STANDARD_MESSAGE_LIST_MISC, &gMiscMessageList);
return 0;
}
@ -402,6 +408,7 @@ void gameReset()
// SFALL
sfallGlobalVarsReset();
sfallListsReset();
messageListRepositoryReset();
}
// 0x442C34
@ -415,6 +422,7 @@ void gameExit()
premadeCharactersExit();
tileDisable();
messageListRepositorySetStandardMessageList(STANDARD_MESSAGE_LIST_MISC, nullptr);
messageListFree(&gMiscMessageList);
combatExit();
gameDialogExit();
@ -448,6 +456,7 @@ void gameExit()
endgameDeathEndingExit();
interfaceFontsExit();
_windowClose();
messageListRepositoryExit();
dbExit();
settingsExit(true);
sfallConfigExit();

View File

@ -201,6 +201,8 @@ int itemsInit()
return -1;
}
messageListRepositorySetStandardMessageList(STANDARD_MESSAGE_LIST_ITEM, &gItemsMessageList);
// SFALL
booksInit();
explosionsInit();
@ -219,6 +221,7 @@ void itemsReset()
// 0x477148
void itemsExit()
{
messageListRepositorySetStandardMessageList(STANDARD_MESSAGE_LIST_ITEM, nullptr);
messageListFree(&gItemsMessageList);
// SFALL

View File

@ -306,6 +306,8 @@ void _map_init()
tickersAdd(gameMouseRefresh);
_gmouse_disable(0);
windowUnhide(gIsoWindow);
messageListRepositorySetStandardMessageList(STANDARD_MESSAGE_LIST_MAP, &gMapMessageList);
}
// 0x482084
@ -314,6 +316,8 @@ void _map_exit()
windowHide(gIsoWindow);
gameMouseSetCursor(MOUSE_CURSOR_ARROW);
tickersRemove(gameMouseRefresh);
messageListRepositorySetStandardMessageList(STANDARD_MESSAGE_LIST_MAP, nullptr);
if (!messageListFree(&gMapMessageList)) {
debugPrint("\nError exiting map_msg_file!");
}

View File

@ -5,6 +5,9 @@
#include <stdlib.h>
#include <string.h>
#include <array>
#include <unordered_map>
#include "debug.h"
#include "memory.h"
#include "platform_compat.h"
@ -17,11 +20,33 @@ namespace fallout {
#define BADWORD_LENGTH_MAX 80
static constexpr int kFirstStandardMessageListId = 0;
static constexpr int kLastStandardMessageListId = kFirstStandardMessageListId + STANDARD_MESSAGE_LIST_COUNT - 1;
static constexpr int kFirstProtoMessageListId = 0x1000;
static constexpr int kLastProtoMessageListId = kFirstProtoMessageListId + PROTO_MESSAGE_LIST_COUNT - 1;
static constexpr int kFirstPersistentMessageListId = 0x2000;
static constexpr int kLastPersistentMessageListId = 0x2FFF;
static constexpr int kFirstTemporaryMessageListId = 0x3000;
static constexpr int kLastTemporaryMessageListId = 0x3FFF;
struct MessageListRepositoryState {
std::array<MessageList*, STANDARD_MESSAGE_LIST_COUNT> standardMessageLists;
std::array<MessageList*, PROTO_MESSAGE_LIST_COUNT> protoMessageLists;
std::unordered_map<int, MessageList*> persistentMessageLists;
std::unordered_map<int, MessageList*> temporaryMessageLists;
int nextTemporaryMessageListId = kFirstTemporaryMessageListId;
};
static bool _message_find(MessageList* msg, int num, int* out_index);
static bool _message_add(MessageList* msg, MessageListItem* new_entry);
static bool _message_parse_number(int* out_num, const char* str);
static int _message_load_field(File* file, char* str);
static MessageList* messageListRepositoryLoad(const char* path);
// 0x50B79C
static char _Error_1[] = "Error";
@ -47,6 +72,8 @@ static char* _message_error_str = _Error_1;
// 0x63207C
static char _bad_copy[MESSAGE_LIST_ITEM_FIELD_MAX_SIZE];
static MessageListRepositoryState* _messageListRepositoryState;
// 0x484770
int badwordsInit()
{
@ -604,4 +631,187 @@ void messageListFilterGenderWords(MessageList* messageList, int gender)
}
}
bool messageListRepositoryInit()
{
_messageListRepositoryState = new (std::nothrow) MessageListRepositoryState();
if (_messageListRepositoryState == nullptr) {
return false;
}
char* fileList;
configGetString(&gSfallConfig, SFALL_CONFIG_MISC_KEY, SFALL_CONFIG_EXTRA_MESSAGE_LISTS_KEY, &fileList);
if (fileList != nullptr && *fileList == '\0') {
fileList = nullptr;
}
char path[COMPAT_MAX_PATH];
int nextMessageListId = 0;
while (fileList != nullptr) {
char* pch = strchr(fileList, ',');
if (pch != nullptr) {
*pch = '\0';
}
char* sep = strchr(fileList, ':');
if (sep != nullptr) {
*sep = '\0';
nextMessageListId = atoi(sep + 1);
}
sprintf(path, "%s\\%s.msg", "game", fileList);
if (sep != nullptr) {
*sep = ':';
}
MessageList* messageList = messageListRepositoryLoad(path);
if (messageList != nullptr) {
_messageListRepositoryState->persistentMessageLists[kFirstPersistentMessageListId + nextMessageListId] = messageList;
}
if (pch != nullptr) {
*pch = ',';
fileList = pch + 1;
} else {
fileList = nullptr;
}
// Sfall's implementation is a little bit odd. |nextMessageListId| can
// be set via "key:value" pair in the config, so if the first pair is
// "msg:12287", then this check will think it's the end of the loop.
// In order to maintain compatibility we'll use the same approach,
// however it looks like the whole idea of auto-numbering extra message
// lists is a bad one. To use these extra message lists we need to
// specify their ids from user-space scripts. Without explicitly
// specifying message list ids as "key:value" pairs a mere change of
// order in the config will break such scripts in an unexpected way.
nextMessageListId++;
if (nextMessageListId == kLastPersistentMessageListId - kFirstPersistentMessageListId + 1) {
break;
}
}
return true;
}
void messageListRepositoryReset()
{
for (auto& pair : _messageListRepositoryState->temporaryMessageLists) {
messageListFree(pair.second);
delete pair.second;
}
_messageListRepositoryState->temporaryMessageLists.clear();
_messageListRepositoryState->nextTemporaryMessageListId = kFirstTemporaryMessageListId;
}
void messageListRepositoryExit()
{
if (_messageListRepositoryState != nullptr) {
for (auto& pair : _messageListRepositoryState->temporaryMessageLists) {
messageListFree(pair.second);
delete pair.second;
}
for (auto& pair : _messageListRepositoryState->persistentMessageLists) {
messageListFree(pair.second);
delete pair.second;
}
delete _messageListRepositoryState;
_messageListRepositoryState = nullptr;
}
}
void messageListRepositorySetStandardMessageList(int standardMessageList, MessageList* messageList)
{
_messageListRepositoryState->standardMessageLists[standardMessageList] = messageList;
}
void messageListRepositorySetProtoMessageList(int protoMessageList, MessageList* messageList)
{
_messageListRepositoryState->protoMessageLists[protoMessageList] = messageList;
}
int messageListRepositoryAddExtra(int messageListId, const char* path)
{
if (messageListId != 0) {
// CE: Probably there is a bug in Sfall, when |messageListId| is
// non-zero, it is enforced to be within persistent id range. That is
// the scripting engine is allowed to add persistent message lists.
// Everything added/changed by scripting engine should be temporary by
// design.
if (messageListId < kFirstPersistentMessageListId || messageListId > kLastPersistentMessageListId) {
return -1;
}
// CE: Sfall stores both persistent and temporary message lists in
// one map, however since we've passed check above, we should only
// check in persistent message lists.
if (_messageListRepositoryState->persistentMessageLists.find(messageListId) != _messageListRepositoryState->persistentMessageLists.end()) {
return 0;
}
} else {
if (_messageListRepositoryState->nextTemporaryMessageListId > kLastTemporaryMessageListId) {
return -3;
}
}
MessageList* messageList = messageListRepositoryLoad(path);
if (messageList == nullptr) {
return -2;
}
if (messageListId == 0) {
messageListId == _messageListRepositoryState->nextTemporaryMessageListId++;
}
_messageListRepositoryState->temporaryMessageLists[messageListId] = messageList;
return messageListId;
}
char* messageListRepositoryGetMsg(int messageListId, int messageId)
{
MessageList* messageList = nullptr;
if (messageListId >= kFirstStandardMessageListId && messageListId <= kLastStandardMessageListId) {
messageList = _messageListRepositoryState->standardMessageLists[messageListId - kFirstStandardMessageListId];
} else if (messageListId >= kFirstProtoMessageListId && messageListId <= kLastProtoMessageListId) {
messageList = _messageListRepositoryState->protoMessageLists[messageListId - kFirstProtoMessageListId];
} else if (messageListId >= kFirstPersistentMessageListId && messageListId <= kLastPersistentMessageListId) {
auto it = _messageListRepositoryState->persistentMessageLists.find(messageListId);
if (it != _messageListRepositoryState->persistentMessageLists.end()) {
messageList = it->second;
}
} else if (messageListId >= kFirstTemporaryMessageListId && messageListId <= kLastTemporaryMessageListId) {
auto it = _messageListRepositoryState->temporaryMessageLists.find(messageListId);
if (it != _messageListRepositoryState->temporaryMessageLists.end()) {
messageList = it->second;
}
}
MessageListItem messageListItem;
return getmsg(messageList, &messageListItem, messageId);
}
static MessageList* messageListRepositoryLoad(const char* path)
{
MessageList* messageList = new (std::nothrow) MessageList();
if (messageList == nullptr) {
return nullptr;
}
if (!messageListInit(messageList)) {
delete messageList;
return nullptr;
}
if (!messageListLoad(messageList, path)) {
delete messageList;
return nullptr;
}
return messageList;
}
} // namespace fallout

View File

@ -7,6 +7,57 @@ namespace fallout {
#define MESSAGE_LIST_ITEM_FIELD_MAX_SIZE 1024
// CE: Working with standard message lists is tricky in Sfall. Many message
// lists are initialized only for the duration of appropriate modal window. This
// is not documented in Sfall and shifts too much responsibility to scripters
// (who should check game mode before accessing volatile message lists). For now
// CE only exposes persistent standard message lists:
// - combat.msg
// - combatai.msg
// - scrname.msg
// - misc.msg
// - item.msg
// - map.msg
// - proto.msg
// - script.msg
// - skill.msg
// - stat.msg
// - trait.msg
// - worldmap.msg
enum StandardMessageList {
STANDARD_MESSAGE_LIST_COMBAT,
STANDARD_MESSAGE_LIST_COMBAT_AI,
STANDARD_MESSAGE_LIST_SCRNAME,
STANDARD_MESSAGE_LIST_MISC,
STANDARD_MESSAGE_LIST_CUSTOM,
STANDARD_MESSAGE_LIST_INVENTORY,
STANDARD_MESSAGE_LIST_ITEM,
STANDARD_MESSAGE_LIST_LSGAME,
STANDARD_MESSAGE_LIST_MAP,
STANDARD_MESSAGE_LIST_OPTIONS,
STANDARD_MESSAGE_LIST_PERK,
STANDARD_MESSAGE_LIST_PIPBOY,
STANDARD_MESSAGE_LIST_QUESTS,
STANDARD_MESSAGE_LIST_PROTO,
STANDARD_MESSAGE_LIST_SCRIPT,
STANDARD_MESSAGE_LIST_SKILL,
STANDARD_MESSAGE_LIST_SKILLDEX,
STANDARD_MESSAGE_LIST_STAT,
STANDARD_MESSAGE_LIST_TRAIT,
STANDARD_MESSAGE_LIST_WORLDMAP,
STANDARD_MESSAGE_LIST_COUNT,
};
enum {
PROTO_MESSAGE_LIST_ITEMS,
PROTO_MESSAGE_LIST_CRITTERS,
PROTO_MESSAGE_LIST_SCENERY,
PROTO_MESSAGE_LIST_TILES,
PROTO_MESSAGE_LIST_WALLS,
PROTO_MESSAGE_LIST_MISC,
PROTO_MESSAGE_LIST_COUNT,
};
typedef struct MessageListItem {
int num;
int flags;
@ -31,6 +82,14 @@ bool messageListFilterBadwords(MessageList* messageList);
void messageListFilterGenderWords(MessageList* messageList, int gender);
bool messageListRepositoryInit();
void messageListRepositoryReset();
void messageListRepositoryExit();
void messageListRepositorySetStandardMessageList(int messageListId, MessageList* messageList);
void messageListRepositorySetProtoMessageList(int messageListId, MessageList* messageList);
int messageListRepositoryAddExtra(int messageListId, const char* path);
char* messageListRepositoryGetMsg(int messageListId, int messageId);
} // namespace fallout
#endif /* MESSAGE_H */

View File

@ -212,6 +212,8 @@ int perksInit()
}
}
messageListRepositorySetStandardMessageList(STANDARD_MESSAGE_LIST_PERK, &gPerksMessageList);
return 0;
}
@ -224,6 +226,7 @@ void perksReset()
// 0x4966B8
void perksExit()
{
messageListRepositorySetStandardMessageList(STANDARD_MESSAGE_LIST_PERK, nullptr);
messageListFree(&gPerksMessageList);
if (gPartyMemberPerkRanks != NULL) {

View File

@ -1111,6 +1111,10 @@ int protoInit()
}
}
for (i = 0; i < 6; i++) {
messageListRepositorySetProtoMessageList(i, &(_proto_msg_files[i]));
}
_mp_critter_stats_list = _aDrugStatSpecia;
_critter_stats_list = _critter_stats_list_strs;
_critter_stats_list_None = _aNone_1;
@ -1181,6 +1185,8 @@ int protoInit()
gBodyTypeNames[i] = getmsg(&gProtoMessageList, &messageListItem, 400 + i);
}
messageListRepositorySetStandardMessageList(STANDARD_MESSAGE_LIST_PROTO, &gProtoMessageList);
return 0;
}
@ -1218,9 +1224,11 @@ void protoExit()
}
for (i = 0; i < 6; i++) {
messageListRepositorySetProtoMessageList(i, nullptr);
messageListFree(&(_proto_msg_files[i]));
}
messageListRepositorySetStandardMessageList(STANDARD_MESSAGE_LIST_PROTO, nullptr);
messageListFree(&gProtoMessageList);
}

View File

@ -1521,6 +1521,8 @@ int scriptsInit()
return -1;
}
messageListRepositorySetStandardMessageList(STANDARD_MESSAGE_LIST_SCRIPT, &gScrMessageList);
return 0;
}
@ -1576,6 +1578,8 @@ int _scr_game_init()
// NOTE: Uninline.
scriptsClearPendingRequests();
messageListRepositorySetStandardMessageList(STANDARD_MESSAGE_LIST_SCRIPT, &gScrMessageList);
return 0;
}
@ -1595,6 +1599,8 @@ int scriptsExit()
{
gScriptsEnabled = false;
_script_engine_run_critters = 0;
messageListRepositorySetStandardMessageList(STANDARD_MESSAGE_LIST_SCRIPT, nullptr);
if (!messageListFree(&gScrMessageList)) {
debugPrint("\nError exiting script message file!");
return -1;
@ -1646,6 +1652,7 @@ int _scr_game_exit()
_scr_remove_all();
programListFree();
tickersRemove(_doBkProcesses);
messageListRepositorySetStandardMessageList(STANDARD_MESSAGE_LIST_SCRIPT, nullptr);
messageListFree(&gScrMessageList);
if (scriptsClearDudeScript() == -1) {
return -1;

View File

@ -49,6 +49,7 @@ bool sfallConfigInit(int argc, char** argv)
configSetInt(&gSfallConfig, SFALL_CONFIG_MISC_KEY, SFALL_CONFIG_BURST_MOD_CENTER_DIVISOR_KEY, SFALL_CONFIG_BURST_MOD_DEFAULT_CENTER_DIVISOR);
configSetInt(&gSfallConfig, SFALL_CONFIG_MISC_KEY, SFALL_CONFIG_BURST_MOD_TARGET_MULTIPLIER_KEY, SFALL_CONFIG_BURST_MOD_DEFAULT_TARGET_MULTIPLIER);
configSetInt(&gSfallConfig, SFALL_CONFIG_MISC_KEY, SFALL_CONFIG_BURST_MOD_TARGET_DIVISOR_KEY, SFALL_CONFIG_BURST_MOD_DEFAULT_TARGET_DIVISOR);
configSetString(&gSfallConfig, SFALL_CONFIG_MISC_KEY, SFALL_CONFIG_EXTRA_MESSAGE_LISTS_KEY, "");
char path[COMPAT_MAX_PATH];
char* executable = argv[0];

View File

@ -59,6 +59,7 @@ namespace fallout {
#define SFALL_CONFIG_TWEAKS_FILE_KEY "TweaksFile"
#define SFALL_CONFIG_GAME_DIALOG_GENDER_WORDS_KEY "DialogGenderWords"
#define SFALL_CONFIG_TOWN_MAP_HOTKEYS_FIX_KEY "TownMapHotkeysFix"
#define SFALL_CONFIG_EXTRA_MESSAGE_LISTS_KEY "ExtraGameMsgFileList"
#define SFALL_CONFIG_BURST_MOD_DEFAULT_CENTER_MULTIPLIER 1
#define SFALL_CONFIG_BURST_MOD_DEFAULT_CENTER_DIVISOR 3

View File

@ -6,6 +6,7 @@
#include "interface.h"
#include "interpreter.h"
#include "item.h"
#include "message.h"
#include "mouse.h"
#include "object.h"
#include "sfall_global_vars.h"
@ -223,6 +224,15 @@ static void opGetStringLength(Program* program)
programStackPushInteger(program, static_cast<int>(strlen(string)));
}
// message_str_game
static void opGetMessage(Program* program)
{
int messageId = programStackPopInteger(program);
int messageListId = programStackPopInteger(program);
char* text = messageListRepositoryGetMsg(messageListId, messageId);
programStackPushString(program, text);
}
// round
static void opRound(Program* program)
{
@ -262,6 +272,7 @@ void sfallOpcodesInit()
interpreterRegisterOpcode(0x8221, opGetScreenHeight);
interpreterRegisterOpcode(0x8237, opParseInt);
interpreterRegisterOpcode(0x824F, opGetStringLength);
interpreterRegisterOpcode(0x826B, opGetMessage);
interpreterRegisterOpcode(0x8267, opRound);
interpreterRegisterOpcode(0x8274, opArtExists);
}

View File

@ -158,6 +158,8 @@ int skillsInit()
// NOTE: Uninline.
skill_use_slot_clear();
messageListRepositorySetStandardMessageList(STANDARD_MESSAGE_LIST_SKILL, &gSkillsMessageList);
return 0;
}
@ -175,6 +177,7 @@ void skillsReset()
// 0x4AA478
void skillsExit()
{
messageListRepositorySetStandardMessageList(STANDARD_MESSAGE_LIST_SKILL, nullptr);
messageListFree(&gSkillsMessageList);
}

View File

@ -131,6 +131,8 @@ int statsInit()
gStatValueDescriptions[index] = getmsg(&gStatsMessageList, &messageListItem, 301 + index);
}
messageListRepositorySetStandardMessageList(STANDARD_MESSAGE_LIST_STAT, &gStatsMessageList);
return 0;
}
@ -146,6 +148,7 @@ int statsReset()
// 0x4AEEE4
int statsExit()
{
messageListRepositorySetStandardMessageList(STANDARD_MESSAGE_LIST_STAT, nullptr);
messageListFree(&gStatsMessageList);
return 0;

View File

@ -85,6 +85,8 @@ int traitsInit()
// NOTE: Uninline.
traitsReset();
messageListRepositorySetStandardMessageList(STANDARD_MESSAGE_LIST_TRAIT, &gTraitsMessageList);
return true;
}
@ -99,6 +101,7 @@ void traitsReset()
// 0x4B3AF8
void traitsExit()
{
messageListRepositorySetStandardMessageList(STANDARD_MESSAGE_LIST_TRAIT, nullptr);
messageListFree(&gTraitsMessageList);
}

View File

@ -858,6 +858,8 @@ int wmWorldMap_init()
citySizeDescription->fid = buildFid(OBJ_TYPE_INTERFACE, 336 + citySize, 0, 0, 0);
}
messageListRepositorySetStandardMessageList(STANDARD_MESSAGE_LIST_WORLDMAP, &wmMsgFile);
return 0;
}
@ -1010,6 +1012,7 @@ void wmWorldMap_exit()
circleBlendTable = NULL;
}
messageListRepositorySetStandardMessageList(STANDARD_MESSAGE_LIST_WORLDMAP, nullptr);
messageListFree(&wmMsgFile);
}