diff --git a/artifacts/scripting/headers/sfall.h b/artifacts/scripting/headers/sfall.h index 4a24582b..c4f20d40 100644 --- a/artifacts/scripting/headers/sfall.h +++ b/artifacts/scripting/headers/sfall.h @@ -207,6 +207,11 @@ #define CURSOR_COMMAND (1) #define CURSOR_TARGETING (2) +// Valid flags for set_rest_mode +#define RESTMODE_DISABLED (1) // disable resting on all maps +#define RESTMODE_STRICT (2) // disable resting on maps with "can_rest_here=No" in Maps.txt, even if there are no other critters +#define RESTMODE_NO_HEALING (4) // disable healing during resting + #define mstr_combat(x) (message_str_game(GAME_MSG_COMBAT, x)) #define mstr_ai(x) (message_str_game(GAME_MSG_AI, x)) #define mstr_scrname(x) (message_str_game(GAME_MSG_SCRNAME, x)) @@ -287,6 +292,7 @@ #define draw_image_scaled(artFile, frame, x, y, w, h) sfall_func6("draw_image_scaled", artFile, frame, x, y, w, h) #define exec_map_update_scripts sfall_func0("exec_map_update_scripts") #define floor2(value) sfall_func1("floor2", value) +#define get_can_rest_on_map(map, elev) sfall_func2("get_can_rest_on_map", map, elev) #define get_current_inven_size(obj) sfall_func1("get_current_inven_size", obj) #define get_current_terrain_name sfall_func0("get_terrain_name") #define get_cursor_mode sfall_func0("get_cursor_mode") @@ -342,6 +348,7 @@ #define real_dude_obj sfall_func0("real_dude_obj") #define remove_all_timer_events sfall_func0("remove_timer_event") #define remove_timer_event(fixedParam) sfall_func1("remove_timer_event", fixedParam) +#define set_can_rest_on_map(map, elev, value) sfall_func3("set_can_rest_on_map", map, elev, value) #define set_car_intface_art(artIndex) sfall_func1("set_car_intface_art", artIndex) #define set_cursor_mode(mode) sfall_func1("set_cursor_mode", mode) #define set_flags(obj, flags) sfall_func2("set_flags", obj, flags) @@ -351,6 +358,8 @@ #define set_object_data(obj, offset, value) sfall_func3("set_object_data", obj, offset, value) #define set_outline(obj, color) sfall_func2("set_outline", obj, color) #define set_quest_failure_value(gvar, threshold) sfall_func2("set_quest_failure_value", gvar, threshold) +#define set_rest_heal_time(time) sfall_func1("set_rest_heal_time", time) +#define set_rest_mode(mode) sfall_func1("set_rest_mode", mode) #define set_scr_name(name) sfall_func1("set_scr_name", name) #define set_terrain_name(x, y, name) sfall_func3("set_terrain_name", x, y, name) #define set_town_title(areaID, title) sfall_func2("set_town_title", areaID, title) diff --git a/artifacts/scripting/sfall function notes.md b/artifacts/scripting/sfall function notes.md index 5dc0e431..d4f041ae 100644 --- a/artifacts/scripting/sfall function notes.md +++ b/artifacts/scripting/sfall function notes.md @@ -642,6 +642,18 @@ sfall_funcX metarule functions - Setting the `tile` to 0 will put the player on the start hex (default tile and elevation) of the map - __NOTE:__ works only in `map_enter_p_proc` procedure +---- +#### set_rest_heal_time +`void sfall_func1("set_rest_heal_time", int minutes)` +- Sets the time interval in minutes for healing during resting. The default is 180 +- The time interval will be reset each time the player reloads the game + +---- +#### set_rest_mode +`void sfall_func1("set_rest_mode", int mode)` +- Sets the bit flags for the rest mode (see `RESTMODE_*` constants in **sfall.h**) +- Passing 0 will reset the rest mode. It will also be reset each time the player reloads the game + ---- #### attack_is_aimed `int sfall_func0("attack_is_aimed")` @@ -685,6 +697,21 @@ sfall_funcX metarule functions - Works just like vanilla `CreateWin` function, but creates a window with `MoveOnTop` flag if the flags argument is not specified, and allows to set additional flags for the created window - `MoveOnTop` flag allows the created window to be placed on top of the game interface +---- +#### set_can_rest_on_map +`void sfall_func3("set_can_rest_on_map", int mapNum, int elev, bool value)` +- Allows/disallows to rest on the map for the specified level, overrides the `can_rest_here` values in **maps.txt** +- `mapNum` is the map index from **maps.txt** +- Passing -1 to the `elev` argument will set the rest value for all map elevations +- The set rest value will be stored in **sfalldb.sav** file (in savegame) + +---- +#### get_can_rest_on_map +`int sfall_func2("get_can_rest_on_map", int mapNum, int elev)` +- Returns the set rest value of the map after using the `set_can_rest_on_map` function +- Returns -1 if the rest value of the map was not previously set (i.e. no data for the map in **sfalldb.sav**) +- The `can_rest_here` values from **maps.txt** are ignored + ---- #### dialog_obj `object sfall_func0("dialog_obj")` diff --git a/sfall/FalloutEngine/FunctionOffsets_def.h b/sfall/FalloutEngine/FunctionOffsets_def.h index b3aa950a..0347d369 100644 --- a/sfall/FalloutEngine/FunctionOffsets_def.h +++ b/sfall/FalloutEngine/FunctionOffsets_def.h @@ -739,6 +739,7 @@ FUNC(wmInterfaceDrawSubTileRectFogged_, 0x4C40A8) FUNC(wmInterfaceInit_, 0x4C2324) FUNC(wmInterfaceRefresh_, 0x4C3830) FUNC(wmInterfaceScrollTabsStart_, 0x4C219C) +FUNC(wmMapCanRestHere_, 0x4BFABC) FUNC(wmMapIsSaveable_, 0x4BFA64) FUNC(wmMapMatchNameToIdx_, 0x4BF9BC) FUNC(wmMapMusicStart_, 0x4C58C0) diff --git a/sfall/Modules/LoadGameHook.cpp b/sfall/Modules/LoadGameHook.cpp index 794421e4..aaf6d73a 100644 --- a/sfall/Modules/LoadGameHook.cpp +++ b/sfall/Modules/LoadGameHook.cpp @@ -169,6 +169,21 @@ static void __stdcall SaveGame2() { goto errorSave; } + GetSavePath(buf, "db"); + h = CreateFileA(buf, GENERIC_WRITE, 0, 0, CREATE_ALWAYS, 0, 0); + if (h != INVALID_HANDLE_VALUE) { + Worldmap::SaveData(h); + data = 0; + WriteFile(h, &data, 8, &size, 0); // padding bytes for compatibility with 4.x + + // last marker + data = 0xCC | VERSION_MAJOR << 8 | VERSION_MINOR << 16 | VERSION_BUILD << 24; + WriteFile(h, &data, 4, &size, 0); + CloseHandle(h); + } else { + goto errorSave; + } + if (FileSystem::IsEmpty()) return; GetSavePath(buf, "fs"); h = CreateFileA(buf, GENERIC_WRITE, 0, 0, CREATE_ALWAYS, 0, 0); @@ -256,6 +271,16 @@ static bool LoadGame_Before() { } else { dlogr("Cannot open sfallgv.sav - assuming non-sfall save.", DL_MAIN); } + + GetSavePath(buf, "db"); + dlogr("Loading data from sfalldb.sav...", DL_MAIN); + h = CreateFileA(buf, GENERIC_READ, FILE_SHARE_READ, 0, OPEN_EXISTING, 0, 0); + if (h != INVALID_HANDLE_VALUE) { + if (Worldmap::LoadData(h)) goto errorLoad; + CloseHandle(h); + } else { + dlogr("Cannot open sfalldb.sav.", DL_MAIN); + } return false; ///////////////////////////////////////////////// diff --git a/sfall/Modules/Scripting/Handlers/Metarule.cpp b/sfall/Modules/Scripting/Handlers/Metarule.cpp index 28b8eab9..296381d9 100644 --- a/sfall/Modules/Scripting/Handlers/Metarule.cpp +++ b/sfall/Modules/Scripting/Handlers/Metarule.cpp @@ -82,6 +82,7 @@ static const SfallMetarule metarules[] = { {"draw_image_scaled", mf_draw_image_scaled, 1, 6, -1, {ARG_INTSTR, ARG_INT, ARG_INT, ARG_INT, ARG_INT, ARG_INT}}, {"exec_map_update_scripts", mf_exec_map_update_scripts, 0, 0}, {"floor2", mf_floor2, 1, 1, 0, {ARG_NUMBER}}, + {"get_can_rest_on_map", mf_get_rest_on_map, 2, 2, -1, {ARG_INT, ARG_INT}}, {"get_current_inven_size", mf_get_current_inven_size, 1, 1, 0, {ARG_OBJECT}}, {"get_cursor_mode", mf_get_cursor_mode, 0, 0}, {"get_flags", mf_get_flags, 1, 1, 0, {ARG_OBJECT}}, @@ -119,6 +120,7 @@ static const SfallMetarule metarules[] = { {"outlined_object", mf_outlined_object, 0, 0}, {"real_dude_obj", mf_real_dude_obj, 0, 0}, {"remove_timer_event", mf_remove_timer_event, 0, 1, -1, {ARG_INT}}, + {"set_can_rest_on_map", mf_set_rest_on_map, 3, 3, -1, {ARG_INT, ARG_INT, ARG_INT}}, {"set_car_intface_art", mf_set_car_intface_art, 1, 1, -1, {ARG_INT}}, {"set_cursor_mode", mf_set_cursor_mode, 1, 1, -1, {ARG_INT}}, {"set_flags", mf_set_flags, 2, 2, -1, {ARG_OBJECT, ARG_INT}}, @@ -128,6 +130,8 @@ static const SfallMetarule metarules[] = { {"set_object_data", mf_set_object_data, 3, 3, -1, {ARG_OBJECT, ARG_INT, ARG_INT}}, {"set_outline", mf_set_outline, 2, 2, -1, {ARG_OBJECT, ARG_INT}}, {"set_quest_failure_value", mf_set_quest_failure_value, 2, 2, -1, {ARG_INT, ARG_INT}}, + {"set_rest_heal_time", mf_set_rest_heal_time, 1, 1, -1, {ARG_INT}}, + {"set_rest_mode", mf_set_rest_mode, 1, 1, -1, {ARG_INT}}, {"set_scr_name", mf_set_scr_name, 0, 1, -1, {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}}, diff --git a/sfall/Modules/Scripting/Handlers/Worldmap.cpp b/sfall/Modules/Scripting/Handlers/Worldmap.cpp index 22e25152..ff1ad78c 100644 --- a/sfall/Modules/Scripting/Handlers/Worldmap.cpp +++ b/sfall/Modules/Scripting/Handlers/Worldmap.cpp @@ -198,6 +198,41 @@ void mf_get_map_enter_position(OpcodeContext& ctx) { ctx.setReturn(id); } +void mf_set_rest_heal_time(OpcodeContext& ctx) { + Worldmap::SetRestHealTime(ctx.arg(0).rawValue()); +} + +void mf_set_rest_mode(OpcodeContext& ctx) { + Worldmap::SetRestMode(ctx.arg(0).rawValue()); +} + +void mf_set_rest_on_map(OpcodeContext& ctx) { + long mapId = ctx.arg(0).rawValue(); + if (mapId < 0) { + ctx.printOpcodeError("%s() - invalid map number argument.", ctx.getMetaruleName()); + ctx.setReturn(-1); + return; + } + long elev = ctx.arg(1).rawValue(); + if (elev < -1 || elev > 2) { + ctx.printOpcodeError("%s() - invalid map elevation argument.", ctx.getMetaruleName()); + ctx.setReturn(-1); + } else { + Worldmap::SetRestMapLevel(mapId, elev, ctx.arg(2).asBool()); + } +} + +void mf_get_rest_on_map(OpcodeContext& ctx) { + long result = -1; + long elev = ctx.arg(1).rawValue(); + if (elev < 0 || elev > 2) { + ctx.printOpcodeError("%s() - invalid map elevation argument.", ctx.getMetaruleName()); + } else { + result = Worldmap::GetRestMapLevel(elev, ctx.arg(0).rawValue()); + } + ctx.setReturn(result); +} + void mf_tile_by_position(OpcodeContext& ctx) { ctx.setReturn(fo::func::tile_num(ctx.arg(0).rawValue(), ctx.arg(1).rawValue())); } diff --git a/sfall/Modules/Scripting/Handlers/Worldmap.h b/sfall/Modules/Scripting/Handlers/Worldmap.h index 432044d4..1ab5115d 100644 --- a/sfall/Modules/Scripting/Handlers/Worldmap.h +++ b/sfall/Modules/Scripting/Handlers/Worldmap.h @@ -48,6 +48,14 @@ void mf_set_map_enter_position(OpcodeContext&); void mf_get_map_enter_position(OpcodeContext&); +void mf_set_rest_heal_time(OpcodeContext&); + +void mf_set_rest_mode(OpcodeContext&); + +void mf_set_rest_on_map(OpcodeContext&); + +void mf_get_rest_on_map(OpcodeContext&); + void mf_tile_by_position(OpcodeContext&); void mf_set_terrain_name(OpcodeContext&); diff --git a/sfall/Modules/Worldmap.cpp b/sfall/Modules/Worldmap.cpp index 9789a281..048dadab 100644 --- a/sfall/Modules/Worldmap.cpp +++ b/sfall/Modules/Worldmap.cpp @@ -17,6 +17,7 @@ */ #include +#include #include #include "..\main.h" @@ -35,9 +36,21 @@ static DWORD AutomapPipboyList[AUTOMAP_MAX]; static DWORD ViewportX; static DWORD ViewportY; +#pragma pack(push, 1) +struct levelRest { + char level[4]; +}; +#pragma pack(pop) + +std::tr1::unordered_map mapRestInfo; + std::vector> wmTerrainTypeNames; // pair first: x + y * number of horizontal sub-tiles std::tr1::unordered_map wmAreaHotSpotTitle; +static bool restMap; +static bool restMode; +static bool restTime; + static bool worldMapLongDelay = false; static DWORD worldMapDelay; static DWORD worldMapTicks; @@ -226,6 +239,24 @@ static __declspec(naked) void PathfinderFix() { } } +static void __declspec(naked) critter_can_obj_dude_rest_hook() { + using namespace fo; + __asm { + push eax; + mov ecx, eax; // elevation + mov edx, dword ptr ds:[FO_VAR_map_number]; + call Worldmap::GetRestMapLevel; + xor edx, edx; + cmp eax, edx; + jge skip; + pop eax; + jmp fo::funcoffs::wmMapCanRestHere_; +skip: + add esp, 4; + retn; // overrides + } +} + static const char* automap = "automap"; // no/yes overrides the value in the table to display the automap in pipboy static void __declspec(naked) wmMapInit_hack() { @@ -324,6 +355,18 @@ end: } } +static void RestRestore() { + if (!restMode) return; + restMode = false; + + SafeWrite8(0x49952C, 0x85); + SafeWrite8(0x497557, 0x85); + SafeWrite8(0x42E587, 0xC7); + SafeWrite32(0x42E588, 0x10C2444); + SafeWrite16(0x499FD4, 0xC201); + SafeWrite16(0x499E93, 0x0574); +} + static void MapLimitsPatches() { long data = IniReader::GetConfigInt("Misc", "LocalMapXLimit", 0); if (data > 0) { @@ -496,6 +539,37 @@ static void PipBoyAutomapsPatch() { dlogr(" Done", DL_INIT); } +void Worldmap::SaveData(HANDLE file) { + DWORD sizeWrite, count = mapRestInfo.size(); + WriteFile(file, &count, 4, &sizeWrite, 0); + std::tr1::unordered_map::iterator it; + for (it = mapRestInfo.begin(); it != mapRestInfo.end(); ++it) { + WriteFile(file, &it->first, 4, &sizeWrite, 0); + WriteFile(file, &it->second, sizeof(levelRest), &sizeWrite, 0); + } +} + +bool Worldmap::LoadData(HANDLE file) { + DWORD count, sizeRead; + + ReadFile(file, &count, 4, &sizeRead, 0); + if (sizeRead != 4) return true; + + for (DWORD i = 0; i < count; i++) { + DWORD mID; + levelRest elevData; + ReadFile(file, &mID, 4, &sizeRead, 0); + ReadFile(file, &elevData, sizeof(levelRest), &sizeRead, 0); + if (sizeRead != sizeof(levelRest)) return true; + mapRestInfo.insert(std::make_pair(mID, elevData)); + } + if (count && !restMap) { + HookCall(0x42E57A, critter_can_obj_dude_rest_hook); + restMap = true; + } + return false; +} + void Worldmap::SetMapMulti(float value) { scriptMapMulti = value; } @@ -512,6 +586,70 @@ void Worldmap::SetCarInterfaceArt(DWORD artIndex) { SafeWrite32(0x4C2D9B, artIndex); } +void Worldmap::SetRestHealTime(DWORD minutes) { + if (minutes > 0) { + SafeWrite32(0x499FDE, minutes); + restTime = (minutes != 180); + } +} + +void Worldmap::SetRestMode(DWORD mode) { + RestRestore(); // restore default + + restMode = ((mode & 0x7) > 0); + if (!restMode) return; + + if (mode & 1) { // bit1 - disable resting on all maps + SafeWrite8(0x49952C, 0x31); // test to xor + SafeWrite8(0x497557, 0x31); + return; + } + if (mode & 2) { // bit2 - disable resting on maps with "can_rest_here=No" in Maps.txt, even if there are no other critters + SafeWrite8(0x42E587, 0xE9); + SafeWrite32(0x42E588, 0x94); // jmp 0x42E620 + } + if (mode & 4) { // bit3 - disable healing during resting + SafeWrite16(0x499FD4, 0x9090); + SafeWrite16(0x499E93, 0x8FEB); // jmp 0x499E24 + } +} + +void Worldmap::SetRestMapLevel(int mapId, long elev, bool canRest) { + std::tr1::unordered_map::iterator keyIt = mapRestInfo.find(mapId); + if (keyIt != mapRestInfo.end()) { + if (elev == -1) { + keyIt->second.level[++elev] = canRest; + keyIt->second.level[++elev] = canRest; + elev++; + } + keyIt->second.level[elev] = canRest; + } else { + if (!restMap) { + HookCall(0x42E57A, critter_can_obj_dude_rest_hook); + restMap = true; + } + + levelRest elevData = {-1, -1, -1, -1}; + if (elev == -1) { + elevData.level[++elev] = canRest; + elevData.level[++elev] = canRest; + elev++; + } + elevData.level[elev] = canRest; + mapRestInfo.insert(std::make_pair(mapId, elevData)); + } +} + +long __fastcall Worldmap::GetRestMapLevel(long elev, int mapId) { + if (mapRestInfo.empty()) return -1; + + std::tr1::unordered_map::iterator keyIt = mapRestInfo.find(mapId); + if (keyIt != mapRestInfo.end()) { + return keyIt->second.level[elev]; + } + return -1; +} + static const char* GetOverrideTerrainName(long x, long y) { if (wmTerrainTypeNames.empty()) return nullptr; @@ -556,6 +694,9 @@ const char* Worldmap::GetCustomAreaTitle(long areaID) { void Worldmap::OnGameLoad() { SetCarInterfaceArt(433); // set index + if (restTime) SetRestHealTime(180); + RestRestore(); + mapRestInfo.clear(); wmTerrainTypeNames.clear(); wmAreaHotSpotTitle.clear(); } diff --git a/sfall/Modules/Worldmap.h b/sfall/Modules/Worldmap.h index ce55e75f..4741dd8c 100644 --- a/sfall/Modules/Worldmap.h +++ b/sfall/Modules/Worldmap.h @@ -28,7 +28,14 @@ public: static void OnGameLoad(); + static void SaveData(HANDLE); + static bool LoadData(HANDLE); + static void SetCarInterfaceArt(DWORD artIndex); + static void SetRestHealTime(DWORD minutes); + static void SetRestMode(DWORD mode); + static void SetRestMapLevel(int mapId, long elev, bool canRest); + static long __fastcall GetRestMapLevel(long elev, int mapId); static DWORD GetAddedYears(bool isCheck = true); static void SetAddedYears(DWORD years);