Implement sfall add_extra_message_file (#327)

https://sfall-team.github.io/sfall/sfall-funcx-macros/#add_extra_msg_file

For now we're choosing to not support the 2-arg form.  We could return `-1` if the script tries to do this, but if so and it doesn't check for an error, that's likely worse than just crashing at this point.  It's essentially an unimplemented opcode.
This commit is contained in:
Mike Klaas
2026-03-25 21:19:35 -07:00
committed by GitHub
parent bb65d8d32a
commit 9d6f86424e
5 changed files with 79 additions and 26 deletions
@@ -0,0 +1,26 @@
#include "test_utils.h"
#include "sfall.h"
// to test, copy test_extra_msg_file.msg into <game folder>/text/English/game/
procedure start begin
variable auto_id;
variable duplicate_auto_id;
display_msg("Testing add_extra_msg_file...");
call assertEquals("metarule exists", metarule_exist("add_extra_msg_file"), 1);
auto_id := add_extra_msg_file("test_extra_msg_file.msg");
call assertNotEquals("auto id is not load error", auto_id, -2);
call assertTrue("auto id lower bound", auto_id >= 0x3000);
call assertTrue("auto id upper bound", auto_id <= 0x3FFF);
call assertEquals("auto message", message_str_game(auto_id, 100), "auto extra msg");
duplicate_auto_id := add_extra_msg_file("TEST_EXTRA_MSG_FILE.MSG");
call assertEquals("duplicate file returns existing id", duplicate_auto_id, auto_id);
call assertEquals("missing file", add_extra_msg_file("missing_extra_msg_file.msg"), -2);
call report_test_results("add_extra_msg_file");
end
+1
View File
@@ -0,0 +1 @@
{100}{}{auto extra msg}
+24 -24
View File
@@ -1,5 +1,6 @@
#include "message.h"
#include <algorithm>
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
@@ -37,6 +38,7 @@ struct MessageListRepositoryState {
std::array<MessageList*, PROTO_MESSAGE_LIST_COUNT> protoMessageLists;
std::unordered_map<int, MessageList*> persistentMessageLists;
std::unordered_map<int, MessageList*> temporaryMessageLists;
std::unordered_map<std::string, int> tempMessageListPaths;
int nextTemporaryMessageListId = kFirstTemporaryMessageListId;
};
@@ -47,6 +49,15 @@ static int _message_load_field(File* file, char* str);
static MessageList* messageListRepositoryLoad(const char* path);
static std::string messageListRepositoryNormalizePath(const char* path)
{
std::string normalizedPath(path != nullptr ? path : "");
std::transform(normalizedPath.begin(), normalizedPath.end(), normalizedPath.begin(), [](unsigned char ch) {
return static_cast<char>(tolower(ch));
});
return normalizedPath;
}
// 0x50B79C
static char _Error_1[] = "Error";
@@ -702,6 +713,7 @@ void messageListRepositoryReset()
delete pair.second;
}
_messageListRepositoryState->temporaryMessageLists.clear();
_messageListRepositoryState->tempMessageListPaths.clear();
_messageListRepositoryState->nextTemporaryMessageListId = kFirstTemporaryMessageListId;
}
@@ -733,28 +745,17 @@ void messageListRepositorySetProtoMessageList(int protoMessageList, MessageList*
_messageListRepositoryState->protoMessageLists[protoMessageList] = messageList;
}
int messageListRepositoryAddExtra(int messageListId, const char* path)
int messageListRepositoryAddExtra(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;
}
std::string normalizedPath = messageListRepositoryNormalizePath(path);
// 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;
}
auto it = _messageListRepositoryState->tempMessageListPaths.find(normalizedPath);
if (it != _messageListRepositoryState->tempMessageListPaths.end()) {
return it->second;
}
if (_messageListRepositoryState->nextTemporaryMessageListId > kLastTemporaryMessageListId) {
return -3;
}
MessageList* messageList = messageListRepositoryLoad(path);
@@ -762,12 +763,11 @@ int messageListRepositoryAddExtra(int messageListId, const char* path)
return -2;
}
if (messageListId == 0) {
messageListId = _messageListRepositoryState->nextTemporaryMessageListId++;
}
int messageListId = _messageListRepositoryState->nextTemporaryMessageListId++;
_messageListRepositoryState->temporaryMessageLists[messageListId] = messageList;
_messageListRepositoryState->tempMessageListPaths[normalizedPath] = messageListId;
return messageListId;
}
+1 -1
View File
@@ -89,7 +89,7 @@ void messageListRepositoryReset();
void messageListRepositoryExit();
void messageListRepositorySetStandardMessageList(int messageListId, MessageList* messageList);
void messageListRepositorySetProtoMessageList(int messageListId, MessageList* messageList);
int messageListRepositoryAddExtra(int messageListId, const char* path);
int messageListRepositoryAddExtra(const char* path);
char* messageListRepositoryGetMsg(int messageListId, int messageId);
} // namespace fallout
+27 -1
View File
@@ -18,6 +18,7 @@
#include "interpreter.h"
#include "inventory.h"
#include "memory.h"
#include "message.h"
#include "object.h"
#include "platform_compat.h"
#include "scripts.h"
@@ -46,6 +47,7 @@ static void mf_get_text_width(Program* program, int args);
static void mf_intface_redraw(Program* program, int args);
static void mf_loot_obj(Program* program, int args);
static void mf_message_box(Program* program, int args);
static void mf_add_extra_msg_file(Program* program, int args);
static void mf_metarule_exist(Program* program, int args);
static void mf_obj_under_cursor(Program* program, int args);
static void mf_opcode_exists(Program* program, int args);
@@ -75,7 +77,7 @@ static const MetaruleInfo* currentMetarule()
// TODO: argument validation, standard error return value
// TODO: reduce code complexity using something like MetaruleContext in sfall
const MetaruleInfo kMetarules[] = {
// {"add_extra_msg_file", mf_add_extra_msg_file, 1, 2, -1, {ARG_STRING, ARG_INT}},
{ "add_extra_msg_file", mf_add_extra_msg_file, 1, 2, -1, { ARG_STRING, ARG_INT } },
// {"add_iface_tag", mf_add_iface_tag, 0, 0},
// {"add_g_timer_event", mf_add_g_timer_event, 2, 2, -1, {ARG_INT, ARG_INT}},
// {"add_trait", mf_add_trait, 1, 1, -1, {ARG_INT}},
@@ -312,6 +314,30 @@ void mf_metarule_exist(Program* program, int args)
programStackPushInteger(program, 0);
}
void mf_add_extra_msg_file(Program* program, int args)
{
if (args == 2) {
programFatalError("op_sfall_func: '%s': explicit fileNumber is not supported in Fallout 2 CE", currentMetarule()->name);
}
const char* fileName = programStackPopString(program);
char path[COMPAT_MAX_PATH];
snprintf(path, sizeof(path), "%s\\%s", "game", fileName);
int result = messageListRepositoryAddExtra(path);
switch (result) {
case -2:
programPrintError("%s() - error loading message file.", currentMetarule()->name);
break;
case -3:
programPrintError("%s() - the limit of adding message files has been exceeded.", currentMetarule()->name);
break;
}
programStackPushInteger(program, result);
}
void mf_opcode_exists(Program* program, int args)
{
int opcode = programStackPopInteger(program);