mirror of
https://github.com/fallout2-ce/fallout2-ce.git
synced 2026-07-27 16:47:11 -07:00
Implement set_unique_id (#466)
* Implement `set_unique_id` * save/load watermark, including compat with previous saves (would be `0`) * metarule to set and reset unique_id * inventory stacking rules
This commit is contained in:
@@ -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<br>unjam_lock<br>set_unjam_locks_time | not implemented | - |
|
||||
| INI settings | get_ini_setting<br>get_ini_string<br>get_ini_section<br>get_ini_sections<br>get_ini_config<br>get_ini_config_db<br>set_ini_setting | ✅ except get_ini_config, get_ini_config_db | `modified_ini` is intentionally omitted as deprecated. |
|
||||
| Objects and scripts | set_self<br>set_dude_obj<br>real_dude_obj<br>remove_script<br>get/set_script<br>obj_is_carrying_obj<br>loot_obj<br>dialog_obj<br>obj_under_cursor<br>get/set_object_data<br>get/set_flags<br>set_unique_id<br>set_scr_name<br>obj_is_openable<br>get/set_proto_data<br>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<br>set_dude_obj<br>real_dude_obj<br>remove_script<br>get/set_script<br>obj_is_carrying_obj<br>loot_obj<br>dialog_obj<br>obj_under_cursor<br>get/set_object_data<br>get/set_flags<br>set_unique_id<br>set_scr_name<br>obj_is_openable<br>get/set_proto_data<br>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<br>stop/resume_game<br>mark_movie_played<br>game_loaded<br>get_game_mode<br>get_uptime<br>signal_close_game | implemented: game_loaded, get_game_mode, get_uptime, signal_close_game | - |
|
||||
| Gameplay tweaks | set_pickpocket_max<br>set_hit_chance_max<br>set_xp_mod<br>set_critter_hit_chance_mod<br>set_base_hit_chance_mod<br>set_hp_per_level_mod<br>gdialog_get_barter_mod<br>get/set_unspent_ap_bonus<br>get/set_unspent_ap_perk_bonus<br>set_base_pickpocket_mod<br>set_critter_pickpocket_mod<br>get/set_inven_ap_cost<br>set_drugs_data<br>get_kill_counter<br>mod_kill_counter<br>set_pipboy_available | implemented: gdialog_get_barter_mod, get/set_unspent_ap{_perk}_bonus, get/set_inven_ap_cost | - |
|
||||
| NPCs | inc_npc_level<br>get_npc_level<br>npc_engine_level_up | not implemented | - |
|
||||
|
||||
@@ -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
|
||||
+12
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
+27
-9
@@ -1,11 +1,13 @@
|
||||
#include "sfall_ext.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
#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;
|
||||
}
|
||||
|
||||
|
||||
+18
-1
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user