diff --git a/SFALL_COMPATIBILITY.md b/SFALL_COMPATIBILITY.md index 99a8bd8f..ce68d89a 100644 --- a/SFALL_COMPATIBILITY.md +++ b/SFALL_COMPATIBILITY.md @@ -82,7 +82,7 @@ See [`https://sfall-team.github.io/sfall/`](https://sfall-team.github.io/sfall/) | Interface / Cursor | get/set_cursor_mode | ✅ | - | | Locks | lock_is_jammed
unjam_lock
set_unjam_locks_time | not implemented | - | | INI settings | get_ini_setting
get_ini_string
get_ini_section
get_ini_sections
get_ini_config
get_ini_config_db
set_ini_setting | ✅ except get_ini_config, get_ini_config_db | `modified_ini` is intentionally omitted as deprecated. | -| Objects and scripts | set_self
set_dude_obj
real_dude_obj
remove_script
get/set_script
obj_is_carrying_obj
loot_obj
dialog_obj
obj_under_cursor
get/set_object_data
get/set_flags
set_unique_id
set_scr_name
obj_is_openable
get/set_proto_data
get_object_ai_data | implemented: set_self, get/set/remove_script, obj_is_carrying_obj, loot_obj, dialog_obj, obj_under_cursor, get_object_data, get_flags, set_flags, obj_is_openable, get_proto_data, set_proto_data | - | +| Objects and scripts | set_self
set_dude_obj
real_dude_obj
remove_script
get/set_script
obj_is_carrying_obj
loot_obj
dialog_obj
obj_under_cursor
get/set_object_data
get/set_flags
set_unique_id
set_scr_name
obj_is_openable
get/set_proto_data
get_object_ai_data | implemented: set_self, get/set/remove_script, obj_is_carrying_obj, loot_obj, dialog_obj, obj_under_cursor, get_object_data, get_flags, set_flags, set_unique_id, obj_is_openable, get_proto_data, set_proto_data | - | | Other / Game management | set_movie_path
stop/resume_game
mark_movie_played
game_loaded
get_game_mode
get_uptime
signal_close_game | implemented: game_loaded, get_game_mode, get_uptime, signal_close_game | - | | Gameplay tweaks | set_pickpocket_max
set_hit_chance_max
set_xp_mod
set_critter_hit_chance_mod
set_base_hit_chance_mod
set_hp_per_level_mod
gdialog_get_barter_mod
get/set_unspent_ap_bonus
get/set_unspent_ap_perk_bonus
set_base_pickpocket_mod
set_critter_pickpocket_mod
get/set_inven_ap_cost
set_drugs_data
get_kill_counter
mod_kill_counter
set_pipboy_available | implemented: gdialog_get_barter_mod, get/set_unspent_ap{_perk}_bonus, get/set_inven_ap_cost | - | | NPCs | inc_npc_level
get_npc_level
npc_engine_level_up | not implemented | - | diff --git a/sfall_testing/gl_test_unique_id.ssl b/sfall_testing/gl_test_unique_id.ssl new file mode 100644 index 00000000..96294a21 --- /dev/null +++ b/sfall_testing/gl_test_unique_id.ssl @@ -0,0 +1,136 @@ +#include "define_lite.h" +#include "sfall.h" +#include "dik.h" +#include "define_extra.h" +#include "lib.inven.h" +#include "test_utils.h" + +#define TEST_ITEM_PID 40 +#define PARTY_UNIQUE_ID_LOWER_BOUND 18000 +#define UNIQUE_ID_LOWER_BOUND 0x10000000 + +procedure find_random_inventory_item_for_unique_id begin + variable items; + variable length; + variable start; + variable scan; + variable index; + variable item; + variable item_id; + + items := inven_as_array(dude_obj); + length := len_array(items); + if (length <= 0) then return 0; + + start := random(0, length - 1); + scan := 0; + while (scan < length) do begin + index := start + scan; + if (index >= length) then index -= length; + + item := items[index]; + item_id := get_object_data(item, OBJ_DATA_ID); + if (item_id < PARTY_UNIQUE_ID_LOWER_BOUND) then begin + return item; + end + + scan += 1; + end + + return items[start]; +end + +procedure assign_unique_id_to_random_inventory_item begin + variable item; + variable old_id; + variable new_id; + + item := find_random_inventory_item_for_unique_id; + if (item == 0) then begin + display_msg("unique_id manual test: inventory is empty"); + return; + end + + old_id := get_object_data(item, OBJ_DATA_ID); + new_id := set_unique_id(item); + + if (new_id == old_id) then begin + display_msg(string_format3("unique_id manual U: %s pid=%d id unchanged=%d", obj_name(item), obj_pid(item), new_id)); + end else begin + display_msg(string_format4("unique_id manual U: %s pid=%d %d->%d", obj_name(item), obj_pid(item), old_id, new_id)); + end +end + +procedure keypress_handler begin + variable pressed := get_sfall_arg_at(0); + variable key := get_sfall_arg_at(1); + + if (not pressed) then return; + + if (key == DIK_U) then begin + call assign_unique_id_to_random_inventory_item(); + end +end + +procedure start begin + variable item_a; + variable item_b; + variable item_c; + variable baseline_total; + variable unique_id_a; + variable unique_id_a_again; + variable unique_id_c; + variable engine_id_a; + variable regular_count; + variable total; + + if (not game_loaded) then return; + + display_msg("Testing set_unique_id..."); + + call assertEquals("metarule exists", metarule_exist("set_unique_id"), 1); + + baseline_total := obj_is_carrying_obj_pid(dude_obj, TEST_ITEM_PID); + item_a := create_object_sid(TEST_ITEM_PID, 0, 0, -1); + item_b := create_object_sid(TEST_ITEM_PID, 0, 0, -1); + item_c := create_object_sid(TEST_ITEM_PID, 0, 0, -1); + + unique_id_a := set_unique_id(item_a); + call assertTrue("unique id lower bound", unique_id_a >= UNIQUE_ID_LOWER_BOUND); + call assertEquals("unique id stored on object", get_object_data(item_a, OBJ_DATA_ID), unique_id_a); + + unique_id_a_again := set_unique_id(item_a); + call assertEquals("existing unique id reused", unique_id_a_again, unique_id_a); + + add_obj_to_inven(dude_obj, item_a); + add_obj_to_inven(dude_obj, item_b); + call assertEquals("unique item stays separate", obj_is_carrying_obj(dude_obj, item_a), 1); + regular_count := obj_is_carrying_obj(dude_obj, item_b); + call assertTrue("regular item still exists after unique item add", regular_count >= 1); + total := obj_is_carrying_obj_pid(dude_obj, TEST_ITEM_PID); + call assertEquals("unique and regular items add two total items", total, baseline_total + 2); + + unique_id_c := set_unique_id(item_c); + call assertNotEquals("second unique item gets new id", unique_id_c, unique_id_a); + add_obj_to_inven(dude_obj, item_c); + call assertEquals("second unique item stays separate", obj_is_carrying_obj(dude_obj, item_c), 1); + + total := obj_is_carrying_obj_pid(dude_obj, TEST_ITEM_PID); + call assertEquals("pid total counts all three added items", total, baseline_total + 3); + + engine_id_a := unset_unique_id(item_a); + call assertTrue("unset returns engine id", engine_id_a < UNIQUE_ID_LOWER_BOUND); + call assertNotEquals("unset replaces unique id", engine_id_a, unique_id_a); + call assertEquals("engine id stored on object", get_object_data(item_a, OBJ_DATA_ID), engine_id_a); + + call assertEquals("remove unique item a", rm_mult_objs_from_inven(dude_obj, item_a, 1), 1); + call assertEquals("remove regular item b", rm_mult_objs_from_inven(dude_obj, item_b, 1), 1); + call assertEquals("remove unique item c", rm_mult_objs_from_inven(dude_obj, item_c, 1), 1); + destroy_object(item_a); + destroy_object(item_b); + destroy_object(item_c); + + call report_test_results("unique_id"); + display_msg("unique_id manual test ready: press U to assign/print a unique id for a random inventory item"); + register_hook_proc(HOOK_KEYPRESS, keypress_handler); +end diff --git a/src/item.cc b/src/item.cc index 984fe001..de29168e 100644 --- a/src/item.cc +++ b/src/item.cc @@ -28,6 +28,7 @@ #include "proto_instance.h" #include "queue.h" #include "random.h" +#include "scripts.h" #include "sfall_config.h" #include "sfall_script_hooks.h" #include "skill.h" @@ -656,10 +657,21 @@ int itemDropAll(Object* critter, int tile) // 0x4779F0 static bool _item_identical(Object* item1, Object* item2) { + if (item1 == item2) { + // This is mostly to make sure the unique_id check below doesn't falsely return + // false when the same item is passed in here. Callers rely on this for checking + // for "is same pointer" + return true; + } + if (item1->pid != item2->pid) { return false; } + if (scriptsIsUniqueObjectId(item1->id) || scriptsIsUniqueObjectId(item2->id)) { + return false; + } + if (item1->sid != item2->sid) { return false; } diff --git a/src/scripts.cc b/src/scripts.cc index 3f4a4f48..9f6524a1 100644 --- a/src/scripts.cc +++ b/src/scripts.cc @@ -193,6 +193,7 @@ static int gScriptsListEntriesLength = 0; // 0x51C7D4 cur_id static int gObjectIdCounter = 4; +static int uniqueObjectIdCounter = OBJECT_ID_UNIQUE_START; // 0x51C7DC count static int gCritterProcessingIndex = 0; @@ -215,6 +216,11 @@ static char* gErrorString = gScriptsErrorText; // 0x51C7F4 blank_str static char* gEmptyString = gScriptsEmptyText; +constexpr int OBJECT_ID_PLAYER = 18000; +// Party member IDs are assigned as (pid & 0xFFFFFF) + OBJECT_ID_PLAYER. +constexpr int OBJECT_ID_PARTY_MEMBER_END = OBJECT_ID_PLAYER + 0x01000000; +constexpr int OBJECT_ID_UNIQUE_END = 0x7FFFFFFF; + // 0x664954 scriptState static unsigned int gScriptsRequests; @@ -556,6 +562,68 @@ int scriptsNewObjectId() return gObjectIdCounter; } +bool scriptsIsUniqueObjectId(int objectId) +{ + return objectId > OBJECT_ID_UNIQUE_START + || (objectId >= OBJECT_ID_PLAYER && objectId < OBJECT_ID_PARTY_MEMBER_END); +} + +int scriptsNewUniqueObjectId() +{ + if (uniqueObjectIdCounter >= OBJECT_ID_UNIQUE_END) { + uniqueObjectIdCounter = OBJECT_ID_UNIQUE_START; + } + + uniqueObjectIdCounter++; + return uniqueObjectIdCounter; +} + +int scriptsGetUniqueObjectIdCounter() +{ + return uniqueObjectIdCounter; +} + +void scriptsResetUniqueObjectIdCounter() +{ + uniqueObjectIdCounter = OBJECT_ID_UNIQUE_START; +} + +void scriptsRestoreUniqueObjectIdCounter(int savedCounter) +{ + if (savedCounter >= OBJECT_ID_UNIQUE_START && savedCounter <= OBJECT_ID_UNIQUE_END) { + uniqueObjectIdCounter = savedCounter; + } else { + uniqueObjectIdCounter = OBJECT_ID_UNIQUE_START; + } +} + +int scriptsSetUniqueObjectId(Object* object) +{ + if (object == nullptr) { + return -1; + } + + if (scriptsIsUniqueObjectId(object->id)) { + return object->id; + } + + object->id = scriptsNewUniqueObjectId(); + scriptsSyncObjectId(object); + return object->id; +} + +void scriptsSyncObjectId(Object* object) +{ + if (object == nullptr || object->sid == -1) { + return; + } + + Script* script; + if (scriptGetScript(object->sid, &script) != -1) { + script->ownerId = object->id; + } +} + // 0x4A390C int scriptGetSid(Program* program) { @@ -1576,6 +1644,7 @@ int _scr_game_init() gScriptsEnabled = true; gGameModeEnabled = 1; gGameTime = 1; + scriptsResetUniqueObjectIdCounter(); gameTimeSetTime(302400); tickersAdd(_doBkProcesses); diff --git a/src/scripts.h b/src/scripts.h index 0040ea23..662d151b 100644 --- a/src/scripts.h +++ b/src/scripts.h @@ -24,6 +24,8 @@ namespace fallout { // 365 * 24 * 60 * 60 * 10 #define GAME_TIME_TICKS_PER_YEAR (315360000) +constexpr int OBJECT_ID_UNIQUE_START = 0x0FFFFFFF; + typedef enum ScriptRequests { SCRIPT_REQUEST_COMBAT = 0x01, SCRIPT_REQUEST_TOWN_MAP = 0x02, @@ -162,6 +164,13 @@ int gameTimeEventProcess(Object* obj, void* data); int _scriptsCheckGameEvents(int* moviePtr, int window); int mapUpdateEventProcess(Object* obj, void* data); int scriptsNewObjectId(); +bool scriptsIsUniqueObjectId(int objectId); +int scriptsNewUniqueObjectId(); +int scriptsSetUniqueObjectId(Object* object); +void scriptsSyncObjectId(Object* object); +int scriptsGetUniqueObjectIdCounter(); +void scriptsResetUniqueObjectIdCounter(); +void scriptsRestoreUniqueObjectIdCounter(int savedCounter); int scriptGetSid(Program* program); Object* scriptGetSelf(Program* program); int scriptSetObjects(int sid, Object* source, Object* target); diff --git a/src/sfall_ext.cc b/src/sfall_ext.cc index a0098d54..961f7645 100644 --- a/src/sfall_ext.cc +++ b/src/sfall_ext.cc @@ -1,11 +1,13 @@ #include "sfall_ext.h" #include +#include #include #include "db.h" #include "debug.h" #include "platform_compat.h" +#include "scripts.h" #include "sfall_arrays.h" #include "sfall_global_vars.h" @@ -103,10 +105,15 @@ bool sfallSaveGameData(File* stream) return false; } - // Write zeros for CE-unimplemented fields: nextObjectId, addedYears, - // fakeTraitsCount, fakePerksCount, fakeSelectablePerksCount - int zero = 0; - for (int i = 0; i < 5; i++) { + if (fileWriteInt32(stream, scriptsGetUniqueObjectIdCounter()) == -1) { + debugPrint("LOADSAVE (SFALL): ** Error saving next object id **\n"); + return false; + } + + // Write zeros for CE-unimplemented fields: addedYears, fakeTraitsCount, + // fakePerksCount, fakeSelectablePerksCount + int32_t zero = 0; + for (int32_t i = 0; i < 4; i++) { if (fileWrite(&zero, sizeof(zero), 1, stream) != 1) { debugPrint("LOADSAVE (SFALL): ** Error saving stub fields **\n"); return false; @@ -133,11 +140,20 @@ bool sfallLoadGameData(File* stream) return false; } - // Skip sections CE doesn't implement: nextObjectId, addedYears, - // fakeTraitsCount, fakePerksCount, fakeSelectablePerksCount - int ignored; - for (int i = 0; i < 5; i++) { - if (fileRead(&ignored, sizeof(ignored), 1, stream) != 1) return true; // old save, stop gracefully + int32_t nextObjectId; + if (fileReadInt32(stream, &nextObjectId) == -1) { + scriptsRestoreUniqueObjectIdCounter(OBJECT_ID_UNIQUE_START); + return true; // old save, stop gracefully + } + + // Skip sections CE doesn't implement: addedYears, fakeTraitsCount, + // fakePerksCount, fakeSelectablePerksCount + int32_t ignored; + for (int32_t i = 0; i < 4; i++) { + if (fileRead(&ignored, sizeof(ignored), 1, stream) != 1) { + scriptsRestoreUniqueObjectIdCounter(nextObjectId); + return true; // old save, stop gracefully + } } if (!sfallArraysLoad(stream)) { @@ -146,6 +162,8 @@ bool sfallLoadGameData(File* stream) return false; } + scriptsRestoreUniqueObjectIdCounter(nextObjectId); + return true; } diff --git a/src/sfall_metarules.cc b/src/sfall_metarules.cc index 8e3c2098..dac101c0 100644 --- a/src/sfall_metarules.cc +++ b/src/sfall_metarules.cc @@ -71,6 +71,7 @@ static void mf_set_cursor_mode(OpcodeContext& ctx); static void mf_set_flags(OpcodeContext& ctx); static void mf_set_iface_tag_text(OpcodeContext& ctx); static void mf_set_outline(OpcodeContext& ctx); +static void mf_set_unique_id(OpcodeContext& ctx); static void mf_show_window(OpcodeContext& ctx); static void mf_signal_close_game(OpcodeContext& ctx); static void mf_tile_by_position(OpcodeContext& ctx); @@ -172,7 +173,7 @@ const MetaruleInfo kMetarules[] = { // {"set_selectable_perk_npc", mf_set_selectable_perk_npc, 5, 5, -1, {ARG_OBJECT, ARG_STRING, ARG_INT, ARG_INT, ARG_STRING}}, // {"set_terrain_name", mf_set_terrain_name, 3, 3, -1, {ARG_INT, ARG_INT, ARG_STRING}}, // {"set_town_title", mf_set_town_title, 2, 2, -1, {ARG_INT, ARG_STRING}}, - // {"set_unique_id", mf_set_unique_id, 1, 2, -1, {ARG_OBJECT, ARG_INT}}, + { "set_unique_id", mf_set_unique_id, 1, 2, -1, { ARG_OBJECT, ARG_INT } }, // {"set_unjam_locks_time", mf_set_unjam_locks_time, 1, 1, -1, {ARG_INT}}, // {"set_window_flag", mf_set_window_flag, 3, 3, -1, {ARG_INTSTR, ARG_INT, ARG_INT}}, { "show_window", mf_show_window, 0, 1, -1, { ARG_STRING } }, @@ -509,6 +510,22 @@ void mf_set_outline(OpcodeContext& ctx) object->outline = outline; } +void mf_set_unique_id(OpcodeContext& ctx) +{ + Object* object = ctx.arg(0).asObject(); + if (ctx.numArgs() > 1 && ctx.arg(1).asInt() == -1) { + // unassign unique_id only if it has one + if (object->id > OBJECT_ID_UNIQUE_START) { + object->id = scriptsNewObjectId(); + scriptsSyncObjectId(object); + } + ctx.setReturn(object->id); + return; + } + + ctx.setReturn(scriptsSetUniqueObjectId(object)); +} + void mf_show_window(OpcodeContext& ctx) { if (ctx.numArgs() == 0) {