Compare commits

..
Author SHA1 Message Date
phobos2077 27805a6c90 mapper: fix cursor being disabled on startup into empty map, replace sscanf with strtol 2026-06-16 14:44:22 +02:00
phobos2077 74769044a6 mapper: code review fixes around proto/spray 2026-06-15 23:19:56 +02:00
phobos2077 a3f66539fb mapper: spray tools & swap prototypes 2026-06-15 22:39:09 +02:00
phobos2077 ff75e13430 mapper: proto_build_all_texts impl 2026-06-15 19:50:52 +02:00
phobos2077 a4a929f620 mapper: reworked writing paths logic
- Drop "target_path_base" and other references to BIS "fallout/dev/" folder in favor of new dev_temp_path setting
- Clear separation between "mapper temp assets" (.cfg, .txt, .tgt), "shipped assets" (.map, .edg, .pro) and "runtime temp assets" (.sav)
- dev_temp_path is mounted as highest priority xbase, so all saved data goes there by default
- as a consequence, play-mode assets (maps with .sav extension and party member .pro files) are explicitly saved to master_patches_path instead of implicitly by using xfileOpen in write mode
2026-06-14 13:29:22 +02:00
phobos2077 f5ba58f27a mapper: proto edit: fix bugs, light refactor and use correct colors 2026-06-13 00:29:09 +02:00
phobos2077 9bd179167c mapper: move proto TXT handling to separate file 2026-06-12 14:50:33 +02:00
phobos2077 9e88b16d71 mapper: fix incorrect strings 2026-06-12 00:37:21 +02:00
phobos2077 15a4f4275b mapper: critter proto editing + wire up all editors 2026-06-12 00:05:34 +02:00
phobos2077 466f12b22e mapper: proto TXT support 2026-06-11 23:45:48 +02:00
phobos2077 26622f3596 mapper: name/description editing 2026-06-11 23:41:07 +02:00
phobos2077 629d1f2b7b mapper: item proto editing 2026-06-11 21:37:20 +02:00
phobos2077 1453a3e294 mapper: scenery proto editing 2026-06-11 21:13:06 +02:00
phobos2077 b5c0fbdf86 mapper: simple proto editing 2026-06-11 21:02:42 +02:00
phobos2077 948558c64c mapper: misc proto editing functions 2026-06-07 11:52:53 +02:00
phobos2077 6412d14a28 mapper: proto_edit_init, proto_remove 2026-06-07 11:15:44 +02:00
phobos2077 6835d10590 mapper: fix TMP map files not being deleted, fix edg not loading when changing maps in play mode 2026-06-07 02:05:57 +02:00
phobos2077 20ad8d8778 mapper: fix SAV files being created in the dev_path 2026-06-07 01:17:01 +02:00
github-actions[bot] 6782a21185 chore: auto-format with clang-format 2026-06-06 21:33:20 +00:00
phobos2077 d12dbca951 mapper: EDG angled edges (squareRect) editing 2026-06-06 23:27:51 +02:00
phobos2077 823858f275 mapper: fix selected object being deleted when opening map 2026-06-06 23:27:50 +02:00
phobos2077 b93591f5d7 EDG: fix incorrect image corruption when scroll limiting kicks in right at the edge border 2026-06-06 23:27:50 +02:00
phobos2077 a91e40e3cc mapper: EDG support (edges only) 2026-06-06 23:27:50 +02:00
phobos2077 7da7d75f30 mapper: move common utility functions to mp_utils.cc 2026-06-06 23:27:50 +02:00
phobos2077 72b4680ff5 mapper: implement dev_path, a path override where saved files will go 2026-06-06 23:27:50 +02:00
Mike Klaas 99a762b649 frm2png can convert to/from indexed pngs (#487)
* frm2png can convert to/from indexed pngs
2026-06-05 20:39:56 -07:00
Mike KlaasandCopilot Autofix powered by AI 1afe925cbf Better fix for directory detection (#486)
* Better fix for directory detection

A couple places we used compat_file_exists to distinguish between files and folders/dirs, but this breaks on POSIX.  This attempts to fix it more holistically

* Make compat_file_exists to be consistent

All platforms now return false for folders/dirs

* Apply suggestions from code review

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-05 20:24:59 -07:00
Mike Klaas 95c07dba9f Fix compile warnings (#482)
Most are of this type

```
warning: 'size' overrides a member function but is not marked 'override' [-Winconsistent-missing-override]

sfall_arrays.cc:313:26: note: overridden virtual function is here
    313 |     virtual ProgramValue GetArrayKey(int index, Program* program) = 0;
        |
```
2026-06-04 08:27:48 -07:00
Mike Klaas 0fc042ef84 Fix save games being missing on Mac OS (#484) 2026-06-03 22:46:57 -07:00
Mike Klaas 29df63dd99 [HRP] Scale movies (#478)
SDL Surface for performance and nice scaling
2026-06-03 21:16:16 -07:00
Mike Klaas 6f2693edf7 Change use_walk_distance to match sfall setting (#481)
* Change `use_walk_distance` to match sfall setting

It's pointless to set this to 0,1 because you don't move if the target is adjacent or yourself, so sfall changes this to "number of spaces" between player and target.  We were reading the old ddraw.ini setting before, incorrectly.

Also add a small optimization to not compute path for adjacent targets

* add comment

* return true when distance is self/adjacent
2026-06-03 15:34:55 -07:00
Mike Klaas e51004a3d1 Ability to use indexed pngs as interface art (#454)
* Ability to load indexed pngs in game

* Switch 2-col inventory .frms to .png
2026-06-02 23:48:08 -07:00
Vlad K 9aa2ae4bbc ZIP DB archive support (#480)
* ZIP: inital support (directly in DBase)

* ZIP: log and bail on any unsupported feature

* dfile.cc compile error fix

* ZIP: minor refactoring

* ZIP: code review fixes and refactoring

* ZIP: simpler and more robust code

* Formatting

* ZIP: remove unnecessary dataOffset normalization
2026-06-02 18:30:20 +02:00
Vlad K 968534314d gameDbInit: change load order to keep master_patches at the top and to match sfall (#479) 2026-06-01 16:57:31 +02:00
Mike Klaas a38f3436f8 A few combat opcodes (#477)
* set_critter_burst_disable

* get/set/reset_critical_table

* `attack_is_aimed`
2026-05-31 16:28:00 -07:00
Mike Klaas 15c22ccdfc HRP/Sfall main menu scaling (#457)
Similar to HRP/Sfall, except:

    Does not support non-aspect-ratio-preserving scale mode
    Uses vanilla art if it fits without scaling (e.g. 640x480 or non-scaling res like 800x480). The scaled-down HR assets looks considerably worse.
2026-05-28 19:49:29 -07:00
Mike Klaas f263075abf Bundle Sfall's gl_highlighting.ssl (#461)
* Bundle Sfall's gl_highlight.ssl

Most mods/distributions assume this is exists since it is bundled with Sfall.  It's also a highly requested feature

I decided to merge this as a first-class game config instead of continuing to read mods/sfall-mods.ini.  I think most people to this to be a default capability that can be toggled in-engine.  Open to other thoughts on the matter.  Lost of people will already have sfall-mods.ini configured, which is a reason to keep it the way it was.

* PR feedback
2026-05-27 23:15:09 -07:00
Mike Klaas c95f91c2ff Fix returnval of critter_mod_skill (#476)
Fixes #474
2026-05-27 22:14:16 -07:00
Vlad K db86e53af2 Fix crash in opTokenize when prev is not null but not present in the source string (#473) 2026-05-27 18:40:54 +02:00
Vlad K 349af8fbb8 Fix aiPacketRead inducing crash when loading saves in certain order (#472)
* Fix aiPacketRead overwriting packet_num from savegames, leading to corrupted state on subsequent loads and crash

* Mark variable as unused and add comment
2026-05-27 10:21:45 +02:00
88 changed files with 9554 additions and 1602 deletions
+8
View File
@@ -614,6 +614,10 @@ if((NOT ANDROID) AND (NOT IOS) AND (NOT CMAKE_SYSTEM_NAME MATCHES "Emscripten"))
target_sources(mapper-ce PUBLIC
${FALLOUT_ENGINE_SOURCES}
${FALLOUT_PLATFORM_SOURCES}
"src/proto_txt.cc"
"src/proto_txt.h"
"src/mapper/map_edge_setup.cc"
"src/mapper/map_edge_setup.h"
"src/mapper/map_func.cc"
"src/mapper/map_func.h"
"src/mapper/mapper.cc"
@@ -624,8 +628,12 @@ if((NOT ANDROID) AND (NOT IOS) AND (NOT CMAKE_SYSTEM_NAME MATCHES "Emscripten"))
"src/mapper/mp_proto.h"
"src/mapper/mp_scrpt.cc"
"src/mapper/mp_scrpt.h"
"src/mapper/mp_spray.cc"
"src/mapper/mp_spray.h"
"src/mapper/mp_targt.cc"
"src/mapper/mp_targt.h"
"src/mapper/mp_utils.cc"
"src/mapper/mp_utils.h"
"src/mapper/mp_text.cc"
"src/mapper/mp_text.h"
)
+13 -5
View File
@@ -76,7 +76,7 @@ For example:
## `ce-frm2png` CLI Tool
There is also a small FRM conversion tool in this repo for exporting Fallout `.frm` art to `.png`.
There is also a small FRM conversion tool in this repo for converting Fallout art between `.frm` and `.png`.
From the repo root, build it with your normal CMake workflow, targeting `ce-frm2png`:
@@ -89,18 +89,26 @@ Basic usage:
1. `./<BUILD_DIR>/ce-frm2png <input.frm> [output.png]`
2. `./<BUILD_DIR>/ce-frm2png <input.frm> [output.png] --palette <path-to-color.pal>`
3. `./<BUILD_DIR>/ce-frm2png <input.frm> [output.png] --frame <index> --direction <index>`
4. `./<BUILD_DIR>/ce-frm2png - [output.png|-] --palette <path-to-color.pal>`
4. `./<BUILD_DIR>/ce-frm2png <input.png> [output.frm] --palette <path-to-color.pal>`
5. `./<BUILD_DIR>/ce-frm2png <input.png> [output.frm] --fps 10 --action-frame 0 --x-offset 0 --y-offset 0`
6. `./<BUILD_DIR>/ce-frm2png <input.png> <output.png> --palette <path-to-color.pal>`
7. `./<BUILD_DIR>/ce-frm2png - [output.png|-] --from-frm --palette <path-to-color.pal>`
8. `./<BUILD_DIR>/ce-frm2png - [output.frm|-] --from-png --palette <path-to-color.pal>`
`-` can be used to take input from stdin (e.g. from `ce-dat-tool cat` or write to stdout).
`-` can be used to take input from stdin (e.g. from `ce-dat-tool cat`) or write to stdout. When reading from stdin, pass `--from-frm` or `--from-png`. Output defaults to PNG for FRM input and FRM for PNG input when output is omitted or `-`; use `--to-png` for PNG output to stdout from PNG input.
If `--palette` isn't specified, it will pick up `color.pal` in the .frm's directory or cwd.
Output file extensions are validated against the selected conversion. For example, PNG-to-FRM output must use `.frm`, and PNG output must use `.png`.
If `--palette` isn't specified, it will pick up `color.pal` in the input file's directory or cwd.
Notes:
- The tool expects a raw `color.pal` file. It does not read palettes directly from `master.dat`, so extract `color.pal` first if needed.
- A typical extraction flow is: `ce-dat-tool master.dat extract --lower . 'color.pal'`
- By default palette index `0` becomes transparent in the output PNG. Pass `--opaque` to keep it opaque.
- The current implementation writes RGBA PNGs. It does not preserve the original Fallout palette indices as a palette-indexed PNG.
- PNG output is always palette-indexed and preserves Fallout palette indices when possible.
- For `png -> frm`, transparent pixels (alpha below 128) map to palette index `0` by default. Pass `--opaque` to quantize every pixel instead.
- The current `png -> frm` implementation writes a single-frame FRM and points all six direction slots at the same frame payload.
## Updating SDL
+9
View File
@@ -153,6 +153,7 @@ In time this stuff will receive in-game interface, right now you have to do it m
* Auto open doors
* 44.1 kHz stereo sound/music supported, in .ogg and .wav format as well as legacy .acm
* Last used save slot is remembered
* Item/Corpse/Container/Critter highlighting (configure using [mods/sfall-mods.ini](https://github.com/sfall-team/sfall/blob/master/artifacts/config_files/sfall-mods.ini))
## Contributing
@@ -162,6 +163,14 @@ Integrating Sfall goodies is the top priority. Quality of life updates are OK to
For current sfall compatibility status and the remaining work needed to close gaps, see [SFALL_COMPATIBILITY.md](SFALL_COMPATIBILITY.md).
## For modders
* Robust (though not 100%) Sfall opcode and hook support
* .ogg/.wav support for map music
* .png support for static assets (must be 8bit indexed)
* .zip support for .dat archives
* Working BIS mapper
## License
The source code is this repository is available under the [Sustainable Use License](LICENSE.md).
+1 -1
View File
@@ -73,7 +73,7 @@ See [`https://sfall-team.github.io/sfall/`](https://sfall-team.github.io/sfall/)
| Arrays / Array functions | create_array<br>temp_array<br>fix_array<br>get/set_array<br>resize_array<br>free_array<br>scan_array<br>len_array<br>save/load_array<br>array_key<br>arrayexpr | âś… | - |
| Perks and traits / NPC perks | set_fake_perk_npc<br>set_fake_trait_npc<br>set_selectable_perk_npc<br>has_fake_perk_npc<br>has_fake_trait_npc | not implemented | - |
| Global scripts / Global script functions | set_global_script_repeat<br>set_global_script_type<br>available_global_script_types | âś… except available_global_script_types | - |
| Combat | attack_is_aimed<br>block_combat<br>force_aimed_shots<br>disable_aimed_shots<br>get_attack_type<br>get/set_bodypart_hit_modifier<br>combat_data<br>get/set/reset_critical_table<br>get_last_target<br>get_last_attacker<br>set_critter_burst_disable<br>get/set_critter_current_ap<br>set_spray_settings<br>get/set_combat_free_move | implemented: get_attack_type, get/set_bodypart_hit_modifier, combat_data, get/set_critter_current_ap, get/set_combat_free_move | - |
| Combat | attack_is_aimed<br>block_combat<br>force_aimed_shots<br>disable_aimed_shots<br>get_attack_type<br>get/set_bodypart_hit_modifier<br>combat_data<br>get/set/reset_critical_table<br>get_last_target<br>get_last_attacker<br>set_critter_burst_disable<br>get/set_critter_current_ap<br>set_spray_settings<br>get/set_combat_free_move | âś… except block_combat, force_aimed_shots, disable_aimed_shots, get_last_target, get_last_attacker, set_spray_settings | - |
| Car | set_car_current_town<br>car_gas_amount<br>set_car_intface_art | implemented: all except set_car_intface_art | - |
| Interface / Windows and images | art_frame_data<br>interface_art_draw<br>interface_print<br>draw_image<br>draw_image_scaled<br>get_window_under_mouse<br>create_win<br>get_window_attribute<br>message_box<br>set_window_flag<br>win_fill_color<br>interface_overlay<br>dialog_message<br>get_text_width<br>hide_window<br>show_window<br>create_message_window | implemented: only message_box, get_text_width, show_window, create_message_window | - |
| Interface / Outline | outlined_object<br>get_outline<br>set_outline | âś… | - |
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.
+12 -2
View File
@@ -119,15 +119,25 @@ numbers_in_dialogue=0
;Set to 2 to also skip the splash screen
skip_opening_movies=0
splash_screen_size=1
;Set to 1 to scale movies to fit the screen while preserving aspect ratio.
;Set to 0 to keep movies at their native size, centered on screen.
movie_aspect_fit=1
;Maximum number of columns shown in the main inventory and loot/steal windows (valid range: 1..2)
;2-column Loot/steal window requires minimum screen width of 673px
inventory_columns=2
;Set to 0 to keep the main menu art and controls at native size.
;Set to 1 to scale only the main menu background image to fit the screen while maintaining aspect ratio.
;Set to 2 to also scale the main menu button panel, red buttons, and button labels with the menu art.
main_menu_scale_mode=1
[qol]
; Automatically open doors that are unlocked and has no script attached.
auto_open_doors=0
; Overrides distance at which walk animation is used when using an object.
use_walk_distance=5
; Overrides distance at which walk animation is used when using an object. 0 disables walking
; This is the number of empty tiles between the player and the target
use_walk_distance=3
; Allow opening party member inventory from the long-press world menu.
party_trade_from_menu=1
; Allow switching to companions' inventories in loot screens (and barter, in the future)
@@ -60,12 +60,14 @@ procedure start begin
display_msg("Testing combat/inventory/object helpers...");
call assertEquals("metarule attack_is_aimed", metarule_exist("attack_is_aimed"), 1);
call assertEquals("metarule get_combat_free_move", metarule_exist("get_combat_free_move"), 1);
call assertEquals("metarule set_combat_free_move", metarule_exist("set_combat_free_move"), 1);
call assertEquals("metarule item_weight", metarule_exist("item_weight"), 1);
call assertEquals("metarule obj_is_openable", metarule_exist("obj_is_openable"), 1);
call assertEquals("opcode get_critter_current_ap", opcode_exists(OP_GET_CRITTER_CURRENT_AP), 1);
call assertEquals("opcode set_critter_current_ap", opcode_exists(OP_SET_CRITTER_CURRENT_AP), 1);
call assertEquals("attack_is_aimed defaults to false", attack_is_aimed, 0);
testItem := create_object_sid(TEST_ITEM_PID, 0, 0, -1);
call assertEquals("item_weight matches proto weight for test item", item_weight(testItem), obj_weight(testItem));
+2
View File
@@ -1043,6 +1043,8 @@ int _action_climb_ladder(Object* critter, Object* ladder)
animationRequestOptions |= ANIMATION_REQUEST_NO_STAND;
reg_anim_begin(animationRequestOptions);
// note: distance is checked to ladder tile, but movement is to SE tile, so the walk/run choice
// could be slightly off
int tile = tileGetTileInDirection(ladder->tile, ROTATION_SE, 1);
if (actionPoints != -1 || objectWithinWalkDistance(critter, ladder)) {
animationRegisterMoveToTile(critter, tile, ladder->elevation, actionPoints, 0);
+196 -9
View File
@@ -1,6 +1,9 @@
#include "art.h"
#include <lodepng.h>
#include <assert.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@@ -134,6 +137,7 @@ static int* gArtCritterFidShoudRunData;
static std::unordered_map<std::string, std::shared_ptr<NamedCacheEntry>> gNamedArtCache;
constexpr int kNamedCacheMaxBytes = 32 * 1024 * 1024; // 32MB soft limit
constexpr size_t kMaxNamedPngPixels = 16 * 1024 * 1024;
static unsigned int gNamedArtCacheMruCounter = 0;
static int gNamedArtCacheCurrentBytes = 0;
@@ -363,6 +367,12 @@ void artToggleObjectTypeHidden(int objectType)
}
}
// art_total
int art_total(int objectType)
{
return objectType >= 0 && objectType < OBJ_TYPE_COUNT ? gArtListDescriptions[objectType].fileNamesLength : 0;
}
// 0x418F7C
int artGetFidgetCount(int headFid)
{
@@ -1116,7 +1126,7 @@ static int artReadFrameData(unsigned char* data, File* stream, int count, int* p
// 0x419E1C
static int artReadHeader(Art* art, File* stream)
{
if (fileReadInt32(stream, &(art->field_0)) == -1) return -1;
if (fileReadInt32(stream, &(art->version)) == -1) return -1;
if (fileReadInt16(stream, &(art->framesPerSecond)) == -1) return -1;
if (fileReadInt16(stream, &(art->actionFrame)) == -1) return -1;
if (fileReadInt16(stream, &(art->frameCount)) == -1) return -1;
@@ -1133,13 +1143,166 @@ static int artReadHeader(Art* art, File* stream)
return 0;
}
// NOTE: Original function was slightly different, but never used. Basically
// it's a memory allocating variant of `artRead` (which reads data into given
// buffer). This function is useful to load custom `frm` files since `Art` now
// needs more memory then it's on-disk size (due to memory padding).
//
// 0x419EC0
Art* artLoad(const char* path)
static bool artPathHasExtension(const char* path, const char* extension)
{
size_t pathLength = strlen(path);
size_t extensionLength = strlen(extension);
if (pathLength < extensionLength) {
return false;
}
return compat_stricmp(path + pathLength - extensionLength, extension) == 0;
}
static bool artReadFile(const char* path, std::vector<unsigned char>& data)
{
int size = 0;
if (dbGetFileSize(path, &size) != 0 || size <= 0) {
return false;
}
data.resize(size);
return dbGetFileContents(path, data.data()) == 0;
}
static bool artUnpackIndexedPngPixels(const std::vector<unsigned char>& indexedData, unsigned width, unsigned height, unsigned bitdepth, unsigned char* output)
{
if (bitdepth != 8) {
return false;
}
size_t pixelCount = static_cast<size_t>(width) * static_cast<size_t>(height);
if (indexedData.size() < pixelCount) {
return false;
}
memcpy(output, indexedData.data(), pixelCount);
return true;
}
static bool artValidateIndexedPngHeader(const char* path, unsigned width, unsigned height, const LodePNGColorMode& color)
{
if (color.colortype != LCT_PALETTE) {
debugPrint("ART: PNG is not palette-indexed: %s\n", path);
return false;
}
if (color.bitdepth != 8) {
debugPrint("ART: indexed PNG bit depth must be 8: %s\n", path);
return false;
}
if (width == 0 || height == 0 || width > SHRT_MAX || height > SHRT_MAX) {
debugPrint("ART: invalid indexed PNG dimensions for %s: %ux%u\n", path, width, height);
return false;
}
size_t pixelCount = static_cast<size_t>(width) * static_cast<size_t>(height);
if (pixelCount > kMaxNamedPngPixels || pixelCount > INT_MAX) {
debugPrint("ART: indexed PNG is too large: %s\n", path);
return false;
}
return true;
}
static bool artIndexedPngHasSupportedTransparency(const LodePNGColorMode& color)
{
if (!lodepng_has_palette_alpha(&color)) {
return true;
}
for (size_t index = 0; index < color.palettesize; index++) {
unsigned char alpha = color.palette[index * 4 + 3];
if (index == 0) {
// palette index 0 is allowed to be either fully transparent or fully opaque
if (alpha != 0 && alpha != 255) {
return false;
}
} else if (alpha != 255) {
return false;
}
}
return true;
}
static Art* artLoadIndexedPng(const char* path)
{
std::vector<unsigned char> encoded;
if (!artReadFile(path, encoded)) {
return nullptr;
}
lodepng::State state;
state.decoder.color_convert = 0;
unsigned width = 0;
unsigned height = 0;
unsigned error = lodepng_inspect(&width, &height, &state, encoded.data(), encoded.size());
if (error != 0) {
debugPrint("ART: failed to inspect indexed PNG %s: %s\n", path, lodepng_error_text(error));
return nullptr;
}
if (!artValidateIndexedPngHeader(path, width, height, state.info_png.color)) {
return nullptr;
}
std::vector<unsigned char> indexedData;
error = lodepng::decode(indexedData, width, height, state, encoded);
if (error != 0) {
debugPrint("ART: failed to decode indexed PNG %s: %s\n", path, lodepng_error_text(error));
return nullptr;
}
if (!artIndexedPngHasSupportedTransparency(state.info_png.color)) {
debugPrint("ART: indexed PNG transparency is unsupported, reserve palette index 0 instead: %s\n", path);
return nullptr;
}
size_t pixelCount = static_cast<size_t>(width) * static_cast<size_t>(height);
Art header = {};
header.version = 4;
header.framesPerSecond = 10;
header.actionFrame = 0;
header.frameCount = 1;
header.dataSize = sizeof(ArtFrame) + static_cast<int>(pixelCount);
int currentPadding = paddingForSize(sizeof(Art));
for (int rotation = 0; rotation < ROTATION_COUNT; rotation++) {
header.dataOffsets[rotation] = 0;
header.padding[rotation] = currentPadding;
}
int dataSize = artGetDataSize(&header);
unsigned char* data = reinterpret_cast<unsigned char*>(internal_malloc(dataSize));
if (data == nullptr) {
return nullptr;
}
memset(data, 0, dataSize);
Art* art = reinterpret_cast<Art*>(data);
*art = header;
ArtFrame* frame = reinterpret_cast<ArtFrame*>(data + sizeof(Art) + art->padding[0]);
frame->width = static_cast<short>(width);
frame->height = static_cast<short>(height);
frame->size = static_cast<int>(pixelCount);
frame->x = 0;
frame->y = 0;
if (!artUnpackIndexedPngPixels(indexedData, width, height, state.info_png.color.bitdepth, reinterpret_cast<unsigned char*>(frame) + sizeof(ArtFrame))) {
debugPrint("ART: failed to read indexed PNG pixels: %s\n", path);
internal_free(data);
return nullptr;
}
return art;
}
static Art* artLoadFrm(const char* path)
{
File* stream = fileOpen(path, "rb");
if (stream == nullptr) {
@@ -1167,6 +1330,30 @@ Art* artLoad(const char* path)
return reinterpret_cast<Art*>(data);
}
// NOTE: Original function was slightly different, but never used. Basically
// it's a memory allocating variant of `artRead` (which reads data into given
// buffer). This function is useful to load custom `frm` files since `Art` now
// needs more memory then it's on-disk size (due to memory padding).
//
// 0x419EC0
Art* artLoad(const char* path)
{
if (path == nullptr) {
return nullptr;
}
if (artPathHasExtension(path, ".png")) {
return artLoadIndexedPng(path);
}
Art* art = artLoadFrm(path);
if (art != nullptr) {
return art;
}
return nullptr;
}
static Art* artLoadLocalized(const char* path)
{
const char* localizedPath;
@@ -1239,7 +1426,7 @@ int artWriteFrameData(unsigned char* data, File* stream, int count)
// 0x41A138
int artWriteHeader(Art* art, File* stream)
{
if (fileWriteInt32(stream, art->field_0) == -1) return -1;
if (fileWriteInt32(stream, art->version) == -1) return -1;
if (fileWriteInt16(stream, art->framesPerSecond) == -1) return -1;
if (fileWriteInt16(stream, art->actionFrame) == -1) return -1;
if (fileWriteInt16(stream, art->frameCount) == -1) return -1;
+2 -1
View File
@@ -69,7 +69,7 @@ typedef enum Background {
} Background;
typedef struct Art {
int field_0;
int version;
short framesPerSecond;
short actionFrame;
short frameCount;
@@ -123,6 +123,7 @@ void artExit();
char* artGetObjectTypeName(int objectType);
int artIsObjectTypeHidden(int objectType);
void artToggleObjectTypeHidden(int objectType);
int art_total(int objectType);
int artGetFidgetCount(int headFid);
void artRender(int fid, unsigned char* dest, int width, int height, int pitch);
int art_list_str(int fid, char* name);
+88 -78
View File
@@ -6254,75 +6254,79 @@ static void criticalsInit()
}
if (mode == 1 || mode == 3) {
char* criticalsConfigFilePath;
configGetString(&gSfallConfig, SFALL_CONFIG_MISC_KEY, SFALL_CONFIG_OVERRIDE_CRITICALS_FILE_KEY, &criticalsConfigFilePath);
if (criticalsConfigFilePath != nullptr && *criticalsConfigFilePath == '\0') {
criticalsConfigFilePath = nullptr;
}
Config criticalsConfig;
if (configInit(&criticalsConfig)) {
char* criticalsConfigFilePath;
configGetString(&gSfallConfig, SFALL_CONFIG_MISC_KEY, SFALL_CONFIG_OVERRIDE_CRITICALS_FILE_KEY, &criticalsConfigFilePath);
if (criticalsConfigFilePath != nullptr && *criticalsConfigFilePath == '\0') {
criticalsConfigFilePath = nullptr;
}
ScopedConfig criticalsConfig(criticalsConfigFilePath, false);
if (criticalsConfig) {
if (mode == 1) {
char sectionKey[16];
if (configRead(&criticalsConfig, criticalsConfigFilePath, false)) {
if (mode == 1) {
char sectionKey[16];
// Read original kill types (19) plus one for the player.
for (int killType = 0; killType < KILL_TYPE_COUNT + 1; killType++) {
for (int hitLocation = 0; hitLocation < HIT_LOCATION_COUNT; hitLocation++) {
for (int effect = 0; effect < CRTICIAL_EFFECT_COUNT; effect++) {
snprintf(sectionKey, sizeof(sectionKey), "c_%02d_%d_%d", killType, hitLocation, effect);
// Read original kill types (19) plus one for the player.
for (int killType = 0; killType < KILL_TYPE_COUNT + 1; killType++) {
for (int hitLocation = 0; hitLocation < HIT_LOCATION_COUNT; hitLocation++) {
for (int effect = 0; effect < CRTICIAL_EFFECT_COUNT; effect++) {
snprintf(sectionKey, sizeof(sectionKey), "c_%02d_%d_%d", killType, hitLocation, effect);
// Update player kill type if needed.
int newKillType = killType == KILL_TYPE_COUNT ? SFALL_KILL_TYPE_COUNT : killType;
for (int dataMember = 0; dataMember < CRIT_DATA_MEMBER_COUNT; dataMember++) {
int value = criticalsGetValue(newKillType, hitLocation, effect, dataMember);
if (configGetInt(criticalsConfig.get(), sectionKey, gCritDataMemberKeys[dataMember], &value)) {
criticalsSetValue(newKillType, hitLocation, effect, dataMember, value);
// Update player kill type if needed.
int newKillType = killType == KILL_TYPE_COUNT ? SFALL_KILL_TYPE_COUNT : killType;
for (int dataMember = 0; dataMember < CRIT_DATA_MEMBER_COUNT; dataMember++) {
int value = criticalsGetValue(newKillType, hitLocation, effect, dataMember);
if (configGetInt(&criticalsConfig, sectionKey, gCritDataMemberKeys[dataMember], &value)) {
criticalsSetValue(newKillType, hitLocation, effect, dataMember, value);
}
}
}
}
}
}
} else if (mode == 3) {
char ktSectionKey[32];
char hitLocationSectionKey[32];
char key[32];
} else if (mode == 3) {
char ktSectionKey[32];
char hitLocationSectionKey[32];
char key[32];
// Read Sfall kill types (38) plus one for the player.
for (int killType = 0; killType < SFALL_KILL_TYPE_COUNT + 1; killType++) {
snprintf(ktSectionKey, sizeof(ktSectionKey), "c_%02d", killType);
// Read Sfall kill types (38) plus one for the player.
for (int killType = 0; killType < SFALL_KILL_TYPE_COUNT + 1; killType++) {
snprintf(ktSectionKey, sizeof(ktSectionKey), "c_%02d", killType);
int enabled = 0;
configGetInt(criticalsConfig.get(), ktSectionKey, "Enabled", &enabled);
if (enabled == 0) {
continue;
}
for (int hitLocation = 0; hitLocation < HIT_LOCATION_COUNT; hitLocation++) {
if (enabled < 2) {
bool hitLocationChanged = false;
snprintf(key, sizeof(key), "Part_%d", hitLocation);
configGetBool(criticalsConfig.get(), ktSectionKey, key, &hitLocationChanged);
if (!hitLocationChanged) {
continue;
}
int enabled = 0;
configGetInt(&criticalsConfig, ktSectionKey, "Enabled", &enabled);
if (enabled == 0) {
continue;
}
snprintf(hitLocationSectionKey, sizeof(hitLocationSectionKey), "c_%02d_%d", killType, hitLocation);
for (int hitLocation = 0; hitLocation < HIT_LOCATION_COUNT; hitLocation++) {
if (enabled < 2) {
bool hitLocationChanged = false;
for (int effect = 0; effect < CRTICIAL_EFFECT_COUNT; effect++) {
for (int dataMember = 0; dataMember < CRIT_DATA_MEMBER_COUNT; dataMember++) {
int value = criticalsGetValue(killType, hitLocation, effect, dataMember);
snprintf(key, sizeof(key), "e%d_%s", effect, gCritDataMemberKeys[dataMember]);
if (configGetInt(criticalsConfig.get(), hitLocationSectionKey, key, &value)) {
criticalsSetValue(killType, hitLocation, effect, dataMember, value);
snprintf(key, sizeof(key), "Part_%d", hitLocation);
configGetBool(&criticalsConfig, ktSectionKey, key, &hitLocationChanged);
if (!hitLocationChanged) {
continue;
}
}
snprintf(hitLocationSectionKey, sizeof(hitLocationSectionKey), "c_%02d_%d", killType, hitLocation);
for (int effect = 0; effect < CRTICIAL_EFFECT_COUNT; effect++) {
for (int dataMember = 0; dataMember < CRIT_DATA_MEMBER_COUNT; dataMember++) {
int value = criticalsGetValue(killType, hitLocation, effect, dataMember);
snprintf(key, sizeof(key), "e%d_%s", effect, gCritDataMemberKeys[dataMember]);
if (configGetInt(&criticalsConfig, hitLocationSectionKey, key, &value)) {
criticalsSetValue(killType, hitLocation, effect, dataMember, value);
}
}
}
}
}
}
}
configFree(&criticalsConfig);
}
}
@@ -6596,40 +6600,46 @@ static void unarmedInitCustom()
{
char* unarmedFileName = nullptr;
configGetString(&gSfallConfig, SFALL_CONFIG_MISC_KEY, SFALL_CONFIG_UNARMED_FILE_KEY, &unarmedFileName);
if (unarmedFileName == nullptr || *unarmedFileName == '\0') {
if (unarmedFileName != nullptr && *unarmedFileName == '\0') {
unarmedFileName = nullptr;
}
if (unarmedFileName == nullptr) {
return;
}
ScopedConfig unarmedConfig(unarmedFileName, false);
if (!unarmedConfig) {
return;
}
Config unarmedConfig;
if (configInit(&unarmedConfig)) {
if (configRead(&unarmedConfig, unarmedFileName, false)) {
char section[4];
char statKey[6];
char section[4];
char statKey[6];
for (int hitMode = 0; hitMode < HIT_MODE_COUNT; hitMode++) {
if (!isUnarmedHitMode(hitMode)) {
continue;
}
for (int hitMode = 0; hitMode < HIT_MODE_COUNT; hitMode++) {
if (!isUnarmedHitMode(hitMode)) {
continue;
UnarmedHitDescription* hitDescription = &(gUnarmedHitDescriptions[hitMode]);
snprintf(section, sizeof(section), "%d", hitMode);
configGetInt(&unarmedConfig, section, "ReqLevel", &(hitDescription->requiredLevel));
configGetInt(&unarmedConfig, section, "SkillLevel", &(hitDescription->requiredSkill));
configGetInt(&unarmedConfig, section, "MinDamage", &(hitDescription->minDamage));
configGetInt(&unarmedConfig, section, "MaxDamage", &(hitDescription->maxDamage));
configGetInt(&unarmedConfig, section, "BonusDamage", &(hitDescription->bonusDamage));
configGetInt(&unarmedConfig, section, "BonusCrit", &(hitDescription->bonusCriticalChance));
configGetInt(&unarmedConfig, section, "APCost", &(hitDescription->actionPointCost));
configGetBool(&unarmedConfig, section, "BonusDamage", &(hitDescription->isPenetrate));
configGetBool(&unarmedConfig, section, "Secondary", &(hitDescription->isSecondary));
for (int stat = 0; stat < PRIMARY_STAT_COUNT; stat++) {
snprintf(statKey, sizeof(statKey), "Stat%d", stat);
configGetInt(&unarmedConfig, section, statKey, &(hitDescription->requiredStats[stat]));
}
}
}
UnarmedHitDescription* hitDescription = &(gUnarmedHitDescriptions[hitMode]);
snprintf(section, sizeof(section), "%d", hitMode);
configGetInt(unarmedConfig.get(), section, "ReqLevel", &(hitDescription->requiredLevel));
configGetInt(unarmedConfig.get(), section, "SkillLevel", &(hitDescription->requiredSkill));
configGetInt(unarmedConfig.get(), section, "MinDamage", &(hitDescription->minDamage));
configGetInt(unarmedConfig.get(), section, "MaxDamage", &(hitDescription->maxDamage));
configGetInt(unarmedConfig.get(), section, "BonusDamage", &(hitDescription->bonusDamage));
configGetInt(unarmedConfig.get(), section, "BonusCrit", &(hitDescription->bonusCriticalChance));
configGetInt(unarmedConfig.get(), section, "APCost", &(hitDescription->actionPointCost));
configGetBool(unarmedConfig.get(), section, "BonusDamage", &(hitDescription->isPenetrate));
configGetBool(unarmedConfig.get(), section, "Secondary", &(hitDescription->isSecondary));
for (int stat = 0; stat < PRIMARY_STAT_COUNT; stat++) {
snprintf(statKey, sizeof(statKey), "Stat%d", stat);
configGetInt(unarmedConfig.get(), section, statKey, &(hitDescription->requiredStats[stat]));
}
configFree(&unarmedConfig);
}
}
+112 -57
View File
@@ -3,6 +3,7 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unordered_set>
#include "actions.h"
#include "animation.h"
@@ -27,7 +28,6 @@
#include "proto.h"
#include "proto_instance.h"
#include "random.h"
#include "scripts.h"
#include "settings.h"
#include "sfall_script_hooks.h"
#include "skill.h"
@@ -51,6 +51,7 @@ static constexpr int kChemUseAnytimeChance = 75;
static constexpr int kChemUseAlwaysChance = 100;
static constexpr int kRandomDrugPickingArraySize = 3;
static std::unordered_set<Object*> burstDisabledCritters;
typedef struct AiMessageRange {
int start;
@@ -378,77 +379,81 @@ int aiInit()
gAiPacketsLength = 0;
ScopedConfig config("data\\ai.txt", true);
if (!config) {
Config config;
if (!configInit(&config)) {
return -1;
}
gAiPackets = (AiPacket*)internal_malloc(sizeof(*gAiPackets) * config.get()->entriesLength);
if (!configRead(&config, "data\\ai.txt", true)) {
return -1;
}
gAiPackets = (AiPacket*)internal_malloc(sizeof(*gAiPackets) * config.entriesLength);
if (gAiPackets == nullptr) {
goto err;
}
for (index = 0; index < config.get()->entriesLength; index++) {
for (index = 0; index < config.entriesLength; index++) {
aiPacketInit(&(gAiPackets[index]));
}
for (index = 0; index < config.get()->entriesLength; index++) {
DictionaryEntry* sectionEntry = &(config.get()->entries[index]);
for (index = 0; index < config.entriesLength; index++) {
DictionaryEntry* sectionEntry = &(config.entries[index]);
AiPacket* ai = &(gAiPackets[index]);
char* stringValue;
ai->name = internal_strdup(sectionEntry->key);
if (!configGetInt(config.get(), sectionEntry->key, "packet_num", &(ai->packet_num))) goto err;
if (!configGetInt(config.get(), sectionEntry->key, "max_dist", &(ai->max_dist))) goto err;
if (!configGetInt(config.get(), sectionEntry->key, "min_to_hit", &(ai->min_to_hit))) goto err;
if (!configGetInt(config.get(), sectionEntry->key, "min_hp", &(ai->min_hp))) goto err;
if (!configGetInt(config.get(), sectionEntry->key, "aggression", &(ai->aggression))) goto err;
if (!configGetInt(&config, sectionEntry->key, "packet_num", &(ai->packet_num))) goto err;
if (!configGetInt(&config, sectionEntry->key, "max_dist", &(ai->max_dist))) goto err;
if (!configGetInt(&config, sectionEntry->key, "min_to_hit", &(ai->min_to_hit))) goto err;
if (!configGetInt(&config, sectionEntry->key, "min_hp", &(ai->min_hp))) goto err;
if (!configGetInt(&config, sectionEntry->key, "aggression", &(ai->aggression))) goto err;
if (configGetString(config.get(), sectionEntry->key, "hurt_too_much", &stringValue)) {
if (configGetString(&config, sectionEntry->key, "hurt_too_much", &stringValue)) {
_parse_hurt_str(stringValue, &(ai->hurt_too_much));
}
if (!configGetInt(config.get(), sectionEntry->key, "secondary_freq", &(ai->secondary_freq))) goto err;
if (!configGetInt(config.get(), sectionEntry->key, "called_freq", &(ai->called_freq))) goto err;
if (!configGetInt(config.get(), sectionEntry->key, "font", &(ai->font))) goto err;
if (!configGetInt(config.get(), sectionEntry->key, "color", &(ai->color))) goto err;
if (!configGetInt(config.get(), sectionEntry->key, "outline_color", &(ai->outline_color))) goto err;
if (!configGetInt(config.get(), sectionEntry->key, "chance", &(ai->chance))) goto err;
if (!configGetInt(config.get(), sectionEntry->key, "run_start", &(ai->run.start))) goto err;
if (!configGetInt(config.get(), sectionEntry->key, "run_end", &(ai->run.end))) goto err;
if (!configGetInt(config.get(), sectionEntry->key, "move_start", &(ai->move.start))) goto err;
if (!configGetInt(config.get(), sectionEntry->key, "move_end", &(ai->move.end))) goto err;
if (!configGetInt(config.get(), sectionEntry->key, "attack_start", &(ai->attack.start))) goto err;
if (!configGetInt(config.get(), sectionEntry->key, "attack_end", &(ai->attack.end))) goto err;
if (!configGetInt(config.get(), sectionEntry->key, "miss_start", &(ai->miss.start))) goto err;
if (!configGetInt(config.get(), sectionEntry->key, "miss_end", &(ai->miss.end))) goto err;
if (!configGetInt(config.get(), sectionEntry->key, "hit_head_start", &(ai->hit[HIT_LOCATION_HEAD].start))) goto err;
if (!configGetInt(config.get(), sectionEntry->key, "hit_head_end", &(ai->hit[HIT_LOCATION_HEAD].end))) goto err;
if (!configGetInt(config.get(), sectionEntry->key, "hit_left_arm_start", &(ai->hit[HIT_LOCATION_LEFT_ARM].start))) goto err;
if (!configGetInt(config.get(), sectionEntry->key, "hit_left_arm_end", &(ai->hit[HIT_LOCATION_LEFT_ARM].end))) goto err;
if (!configGetInt(config.get(), sectionEntry->key, "hit_right_arm_start", &(ai->hit[HIT_LOCATION_RIGHT_ARM].start))) goto err;
if (!configGetInt(config.get(), sectionEntry->key, "hit_right_arm_end", &(ai->hit[HIT_LOCATION_RIGHT_ARM].end))) goto err;
if (!configGetInt(config.get(), sectionEntry->key, "hit_torso_start", &(ai->hit[HIT_LOCATION_TORSO].start))) goto err;
if (!configGetInt(config.get(), sectionEntry->key, "hit_torso_end", &(ai->hit[HIT_LOCATION_TORSO].end))) goto err;
if (!configGetInt(config.get(), sectionEntry->key, "hit_right_leg_start", &(ai->hit[HIT_LOCATION_RIGHT_LEG].start))) goto err;
if (!configGetInt(config.get(), sectionEntry->key, "hit_right_leg_end", &(ai->hit[HIT_LOCATION_RIGHT_LEG].end))) goto err;
if (!configGetInt(config.get(), sectionEntry->key, "hit_left_leg_start", &(ai->hit[HIT_LOCATION_LEFT_LEG].start))) goto err;
if (!configGetInt(config.get(), sectionEntry->key, "hit_left_leg_end", &(ai->hit[HIT_LOCATION_LEFT_LEG].end))) goto err;
if (!configGetInt(config.get(), sectionEntry->key, "hit_eyes_start", &(ai->hit[HIT_LOCATION_EYES].start))) goto err;
if (!configGetInt(config.get(), sectionEntry->key, "hit_eyes_end", &(ai->hit[HIT_LOCATION_EYES].end))) goto err;
if (!configGetInt(config.get(), sectionEntry->key, "hit_groin_start", &(ai->hit[HIT_LOCATION_GROIN].start))) goto err;
if (!configGetInt(config.get(), sectionEntry->key, "hit_groin_end", &(ai->hit[HIT_LOCATION_GROIN].end))) goto err;
if (!configGetInt(&config, sectionEntry->key, "secondary_freq", &(ai->secondary_freq))) goto err;
if (!configGetInt(&config, sectionEntry->key, "called_freq", &(ai->called_freq))) goto err;
if (!configGetInt(&config, sectionEntry->key, "font", &(ai->font))) goto err;
if (!configGetInt(&config, sectionEntry->key, "color", &(ai->color))) goto err;
if (!configGetInt(&config, sectionEntry->key, "outline_color", &(ai->outline_color))) goto err;
if (!configGetInt(&config, sectionEntry->key, "chance", &(ai->chance))) goto err;
if (!configGetInt(&config, sectionEntry->key, "run_start", &(ai->run.start))) goto err;
if (!configGetInt(&config, sectionEntry->key, "run_end", &(ai->run.end))) goto err;
if (!configGetInt(&config, sectionEntry->key, "move_start", &(ai->move.start))) goto err;
if (!configGetInt(&config, sectionEntry->key, "move_end", &(ai->move.end))) goto err;
if (!configGetInt(&config, sectionEntry->key, "attack_start", &(ai->attack.start))) goto err;
if (!configGetInt(&config, sectionEntry->key, "attack_end", &(ai->attack.end))) goto err;
if (!configGetInt(&config, sectionEntry->key, "miss_start", &(ai->miss.start))) goto err;
if (!configGetInt(&config, sectionEntry->key, "miss_end", &(ai->miss.end))) goto err;
if (!configGetInt(&config, sectionEntry->key, "hit_head_start", &(ai->hit[HIT_LOCATION_HEAD].start))) goto err;
if (!configGetInt(&config, sectionEntry->key, "hit_head_end", &(ai->hit[HIT_LOCATION_HEAD].end))) goto err;
if (!configGetInt(&config, sectionEntry->key, "hit_left_arm_start", &(ai->hit[HIT_LOCATION_LEFT_ARM].start))) goto err;
if (!configGetInt(&config, sectionEntry->key, "hit_left_arm_end", &(ai->hit[HIT_LOCATION_LEFT_ARM].end))) goto err;
if (!configGetInt(&config, sectionEntry->key, "hit_right_arm_start", &(ai->hit[HIT_LOCATION_RIGHT_ARM].start))) goto err;
if (!configGetInt(&config, sectionEntry->key, "hit_right_arm_end", &(ai->hit[HIT_LOCATION_RIGHT_ARM].end))) goto err;
if (!configGetInt(&config, sectionEntry->key, "hit_torso_start", &(ai->hit[HIT_LOCATION_TORSO].start))) goto err;
if (!configGetInt(&config, sectionEntry->key, "hit_torso_end", &(ai->hit[HIT_LOCATION_TORSO].end))) goto err;
if (!configGetInt(&config, sectionEntry->key, "hit_right_leg_start", &(ai->hit[HIT_LOCATION_RIGHT_LEG].start))) goto err;
if (!configGetInt(&config, sectionEntry->key, "hit_right_leg_end", &(ai->hit[HIT_LOCATION_RIGHT_LEG].end))) goto err;
if (!configGetInt(&config, sectionEntry->key, "hit_left_leg_start", &(ai->hit[HIT_LOCATION_LEFT_LEG].start))) goto err;
if (!configGetInt(&config, sectionEntry->key, "hit_left_leg_end", &(ai->hit[HIT_LOCATION_LEFT_LEG].end))) goto err;
if (!configGetInt(&config, sectionEntry->key, "hit_eyes_start", &(ai->hit[HIT_LOCATION_EYES].start))) goto err;
if (!configGetInt(&config, sectionEntry->key, "hit_eyes_end", &(ai->hit[HIT_LOCATION_EYES].end))) goto err;
if (!configGetInt(&config, sectionEntry->key, "hit_groin_start", &(ai->hit[HIT_LOCATION_GROIN].start))) goto err;
if (!configGetInt(&config, sectionEntry->key, "hit_groin_end", &(ai->hit[HIT_LOCATION_GROIN].end))) goto err;
ai->hit[HIT_LOCATION_GROIN].end++;
if (configGetString(config.get(), sectionEntry->key, "area_attack_mode", &stringValue)) {
if (configGetString(&config, sectionEntry->key, "area_attack_mode", &stringValue)) {
_cai_match_str_to_list(stringValue, gAreaAttackModeKeys, AREA_ATTACK_MODE_COUNT, &(ai->area_attack_mode));
} else {
ai->area_attack_mode = -1;
}
if (configGetString(config.get(), sectionEntry->key, "run_away_mode", &stringValue)) {
if (configGetString(&config, sectionEntry->key, "run_away_mode", &stringValue)) {
_cai_match_str_to_list(stringValue, gRunAwayModeKeys, RUN_AWAY_MODE_COUNT, &(ai->run_away_mode));
if (ai->run_away_mode >= 0) {
@@ -456,47 +461,49 @@ int aiInit()
}
}
if (configGetString(config.get(), sectionEntry->key, "best_weapon", &stringValue)) {
if (configGetString(&config, sectionEntry->key, "best_weapon", &stringValue)) {
_cai_match_str_to_list(stringValue, gBestWeaponKeys, BEST_WEAPON_COUNT, &(ai->best_weapon));
}
if (configGetString(config.get(), sectionEntry->key, "distance", &stringValue)) {
if (configGetString(&config, sectionEntry->key, "distance", &stringValue)) {
_cai_match_str_to_list(stringValue, gDistanceModeKeys, DISTANCE_COUNT, &(ai->distance));
}
if (configGetString(config.get(), sectionEntry->key, "attack_who", &stringValue)) {
if (configGetString(&config, sectionEntry->key, "attack_who", &stringValue)) {
_cai_match_str_to_list(stringValue, gAttackWhoKeys, ATTACK_WHO_COUNT, &(ai->attack_who));
}
if (configGetString(config.get(), sectionEntry->key, "chem_use", &stringValue)) {
if (configGetString(&config, sectionEntry->key, "chem_use", &stringValue)) {
_cai_match_str_to_list(stringValue, gChemUseKeys, CHEM_USE_COUNT, &(ai->chem_use));
}
configGetIntList(config.get(), sectionEntry->key, "chem_primary_desire", ai->chem_primary_desire, AI_PACKET_CHEM_PRIMARY_DESIRE_COUNT);
configGetIntList(&config, sectionEntry->key, "chem_primary_desire", ai->chem_primary_desire, AI_PACKET_CHEM_PRIMARY_DESIRE_COUNT);
if (configGetString(config.get(), sectionEntry->key, "disposition", &stringValue)) {
if (configGetString(&config, sectionEntry->key, "disposition", &stringValue)) {
_cai_match_str_to_list(stringValue, gDispositionKeys, DISPOSITION_COUNT, &(ai->disposition));
ai->disposition--;
}
if (configGetString(config.get(), sectionEntry->key, "body_type", &stringValue)) {
if (configGetString(&config, sectionEntry->key, "body_type", &stringValue)) {
ai->body_type = internal_strdup(stringValue);
} else {
ai->body_type = nullptr;
}
if (configGetString(config.get(), sectionEntry->key, "general_type", &stringValue)) {
if (configGetString(&config, sectionEntry->key, "general_type", &stringValue)) {
ai->general_type = internal_strdup(stringValue);
} else {
ai->general_type = nullptr;
}
}
if (index < config.get()->entriesLength) {
if (index < config.entriesLength) {
goto err;
}
gAiPacketsLength = config.get()->entriesLength;
gAiPacketsLength = config.entriesLength;
configFree(&config);
gAiInitialized = true;
@@ -505,7 +512,7 @@ int aiInit()
err:
if (gAiPackets != nullptr) {
for (index = 0; index < config.get()->entriesLength; index++) {
for (index = 0; index < config.entriesLength; index++) {
AiPacket* ai = &(gAiPackets[index]);
if (ai->name != nullptr) {
internal_free(ai->name);
@@ -519,12 +526,15 @@ err:
debugPrint("Error processing ai.txt");
configFree(&config);
return -1;
}
// 0x4279F8
void aiReset()
{
burstDisabledCritters.clear();
}
// 0x4279FC
@@ -607,7 +617,14 @@ int aiSave(File* stream)
// 0x427BC8
static int aiPacketRead(File* stream, AiPacket* ai)
{
if (fileReadInt32(stream, &(ai->packet_num)) == -1) return -1;
int packetNum;
if (fileReadInt32(stream, &packetNum) == -1) return -1;
// Consume the serialized packet number to preserve save compatibility,
// but do not overwrite the value in AiPacket to avoid corrupted data on save loads.
// This might happen if saves disagree on packet_num and packets are loaded from AI.TXT only on game init.
(void)packetNum;
if (fileReadInt32(stream, &(ai->max_dist)) == -1) return -1;
if (fileReadInt32(stream, &(ai->min_to_hit)) == -1) return -1;
if (fileReadInt32(stream, &(ai->min_hp)) == -1) return -1;
@@ -881,6 +898,37 @@ int aiSetChemUse(Object* critter, int chemUse)
return 0;
}
bool aiIsBurstDisabled(Object* critter)
{
if (critter == nullptr) {
return false;
}
return burstDisabledCritters.find(critter) != burstDisabledCritters.end();
}
void aiSetBurstDisabled(Object* critter, bool disable)
{
if (critter == nullptr || FID_TYPE(critter->fid) != OBJ_TYPE_CRITTER || critter == gDude) {
return;
}
if (disable) {
burstDisabledCritters.insert(critter);
} else {
burstDisabledCritters.erase(critter);
}
}
void aiRemoveBurstDisabled(Object* critter)
{
if (critter == nullptr) {
return;
}
burstDisabledCritters.erase(critter);
}
// 0x428340
int aiGetDisposition(Object* obj)
{
@@ -2278,6 +2326,13 @@ static int _ai_pick_hit_mode(Object* attacker, Object* weapon, Object* defender)
return HIT_MODE_RIGHT_WEAPON_PRIMARY;
}
if (aiIsBurstDisabled(attacker)) {
int secondaryAnimation = weaponGetAnimationForHitMode(weapon, HIT_MODE_RIGHT_WEAPON_SECONDARY);
if (secondaryAnimation == ANIM_FIRE_BURST || secondaryAnimation == ANIM_FIRE_CONTINUOUS) {
return HIT_MODE_RIGHT_WEAPON_PRIMARY;
}
}
bool useSecondaryMode = false;
AiPacket* ai = aiGetPacket(attacker);
+3
View File
@@ -45,6 +45,9 @@ int aiSetBestWeapon(Object* critter, int bestWeapon);
int aiSetDistance(Object* critter, int distance);
int aiSetAttackWho(Object* critter, int attackWho);
int aiSetChemUse(Object* critter, int chemUse);
bool aiIsBurstDisabled(Object* critter);
void aiSetBurstDisabled(Object* critter, bool disable);
void aiRemoveBurstDisabled(Object* critter);
int aiGetDisposition(Object* obj);
int aiSetDisposition(Object* obj, int a2);
int _caiSetupTeamCombat(Object* attackerTeam, Object* defenderTeam);
+5 -2
View File
@@ -851,7 +851,6 @@ bool configSetBool(Config* config, const char* sectionKey, const char* key, bool
return configSetInt(config, sectionKey, key, value ? 1 : 0);
}
// ScopedConfig is a RAII wrapper around Config that makes cleanup easier.
ScopedConfig::ScopedConfig()
{
_loaded = configInit(&_config);
@@ -872,7 +871,11 @@ ScopedConfig::~ScopedConfig()
}
}
// get returns a pointer to be useful in the config* API above; it cannot be nullptr
bool ScopedConfig::isInitialized() const
{
return _config.isInitialized();
}
Config* ScopedConfig::get()
{
return &_config;
+24
View File
@@ -144,6 +144,30 @@ File* fileOpen(const char* filename, const char* mode)
return xfileOpen(filename, mode);
}
File* fileOpenDirect(const char* filename, const char* mode)
{
return xfileOpenDirect(filename, mode);
}
const char* buildDataPath(const char* root, const char* relative)
{
static char path[COMPAT_MAX_PATH];
size_t rootLen = strlen(root);
bool rootHasSeparator = rootLen > 0 && (root[rootLen - 1] == '\\' || root[rootLen - 1] == '/');
snprintf(path, sizeof(path), "%s%s%s", root, rootHasSeparator ? "" : "\\", relative);
char dir[COMPAT_MAX_PATH];
snprintf(dir, sizeof(dir), "%s", path);
char* separator = strrchr(dir, '\\');
if (separator != nullptr) {
*separator = '\0';
compat_mkdir_recursive(dir);
}
return path;
}
// 0x4C5ED0 db_fprintf
int filePrintFormatted(File* stream, const char* format, ...)
{
+5
View File
@@ -18,6 +18,7 @@ int dbGetFileSize(const char* filePath, int* sizePtr);
int dbGetFileContents(const char* filePath, void* ptr);
int fileClose(File* stream);
File* fileOpen(const char* filename, const char* mode);
File* fileOpenDirect(const char* filename, const char* mode);
int filePrintFormatted(File* stream, const char* format, ...);
int fileReadChar(File* stream);
char* fileReadString(char* str, size_t size, File* stream);
@@ -111,6 +112,10 @@ void fileNameListFree(char*** fileNames, int unused);
int fileGetSize(File* stream);
void fileSetReadProgressHandler(FileReadProgressHandler* handler, int size);
// Joins [root] and [relative] into a save path and creates its directory.
// Returns a pointer to a static buffer.
const char* buildDataPath(const char* root, const char* relative);
} // namespace fallout
#endif /* DB_H */

Some files were not shown because too many files have changed in this diff Show More