From a05ab5bd8f3614cf91d78353e374916108bc14ea Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Fri, 4 Sep 2026 20:35:05 -0400 Subject: [PATCH 1/2] hold bitmap references for replacement and script-set textures Model instance replacement arrays and script-set model textures stored raw bitmap handles without taking a load-count reference. A script doing ship.Textures["foo"] = gr.loadTexture("bar") therefore left the ship pointing at a bitmap slot that was freed as soon as Lua collected the temporary texture handle, and reused by whatever bitmap loaded next. - Add bm_add_ref() / bm_release_ref() to bmpman so callers no longer need BMPMAN_INTERNAL to take a reference. Both count on the first frame of an animation, so any frame may be passed and the release always hits the same entry as the add; neither counts render targets, since bm_release() will not release those anyway. Use them in the scripting texture handle and the librocket rendering interface, which previously counted on whatever frame they were handed. - Add bm_page_out() (bm_unload() with a new keep_reference option) for dropping a bitmap's data without giving up the caller's reference, and use it when paging out model textures and glow bank bitmaps. The plain bm_unload() call used before consumed one reference without freeing when another holder remained, which was harmless while nothing ever released but would now leave the model with a dangling handle once the other holder did. - Make model_texture_replace own its entries: the array is now private, is written only through adopt() / reference() / clear(), and releases every handle it holds when destroyed. Convert every writer (mission and SEXP replacements, cockpit displays, scripting, qtFRED). Render targets are the documented exception; the cockpit display code no longer pretends to release its target separately. - Release the references that mission parsing and the texture replacement SEXPs take on replacement bitmaps when the parse objects are discarded; ships created from those objects hold their own. Previously those references were never released. - Add model_instance_load_replacement_textures() to load a table's replacement list into an instance by filename, and use it from ship_model_change, the lab, FRED, and qtFRED, which each had their own copy of that loop (and which each leaked the loader's references). - Give texture_info an optional held reference so script-set model textures stay alive until reset, replaced, or paged out. PageOut() now pages out or releases the texture the model loaded rather than whatever is currently drawn, which also stops the debris species swap from releasing a texture it does not own. - Have the cached UI render instances carry their ship class's replacement textures, loaded once in model_set_up_techroom_instance(), and add model_get_cached_ui_render_instance_for_class() so that the tech room, ship and weapon select, loadout icons, and tech model rendering fetch the instance and its replacements with one call instead of each building a fresh array with bm_load() every frame, which leaked one load count per frame. The instance cache is now keyed by ship class as well, since classes sharing a model may differ in replacement textures. As a side effect, those screens and the briefing closeup now load table replacement textures the same way missions do, so "invisible" and animated replacements are honored in previews where they were previously ignored. - Delete the briefing closeup's model instance when its icon is set up again or the briefing closes; previously every revisited icon leaked its instance, which now also pinned its replacement bitmaps. - Skip a cockpit display whose texture is not on the cockpit model, with a warning; previously this indexed the replacement array with -1. - Guard bmpman against static destructors that release bitmaps after an exit that skipped bm_close(). - Document the texture slot layout of the "textures" and "modelinstancetextures" Lua handles, fix the stale TM_NUM_TYPES comment, and correct the gr.loadTexture docs on texture lifetime. - Glow bank bitmaps are now released when a model is unloaded, and support ships receive their class's replacement textures. Co-Authored-By: Claude Fable 5.1 --- code/bmpman/bmpman.cpp | 74 ++++++++++- code/bmpman/bmpman.h | 56 +++++++- code/lab/manager/lab_manager.cpp | 17 +-- code/menuui/techmenu.cpp | 12 +- code/mission/missionparse.cpp | 85 +++++++++--- code/mission/missionparse.h | 3 + code/missionui/missionbrief.cpp | 39 +++++- code/missionui/missionscreencommon.cpp | 19 +-- code/missionui/missionshipchoice.cpp | 5 - code/missionui/missionweaponchoice.cpp | 10 +- code/model/model.h | 57 ++++++-- code/model/modelinterp.cpp | 124 +++++++++++++----- code/model/modelread.cpp | 56 +++++++- code/model/modelrender.cpp | 92 +++++++------ code/model/modelrender.h | 6 +- code/scpui/RocketRenderingInterface.cpp | 11 +- code/scripting/api/libs/graphics.cpp | 5 +- code/scripting/api/objs/model.cpp | 8 +- code/scripting/api/objs/modelinstance.cpp | 12 +- code/scripting/api/objs/shipclass.cpp | 4 - code/scripting/api/objs/texture.cpp | 14 +- code/scripting/api/objs/texture.h | 3 +- code/scripting/api/objs/texturemap.cpp | 6 +- code/ship/ship.cpp | 50 +++---- code/ship/ship.h | 4 + fred2/management.cpp | 12 +- freespace2/freespace.cpp | 1 + qtfred/src/mission/Editor.cpp | 12 +- .../ShipTextureReplacementDialogModel.cpp | 13 +- 29 files changed, 537 insertions(+), 273 deletions(-) diff --git a/code/bmpman/bmpman.cpp b/code/bmpman/bmpman.cpp index 9b9788adb67..47c93214d78 100644 --- a/code/bmpman/bmpman.cpp +++ b/code/bmpman/bmpman.cpp @@ -81,6 +81,13 @@ SCP_vector> bm_blocks; // -------------------------------------------------------------------------------------------------------------------- // Definition of private variables at file scope (static). static bool bm_inited = false; + +// Some objects with static storage duration (e.g. model_texture_replace held by a global shared_ptr) release bitmaps +// in their destructors. If the program exits without bm_close(), bm_blocks may be destroyed before they are, so this +// guard, which is destroyed before bm_blocks because it is defined after it, marks bmpman as uninitialized first. +static struct bm_static_destruction_guard { + ~bm_static_destruction_guard() { bm_inited = false; } +} Bm_static_destruction_guard; static uint Bm_next_signature = 0x1234; static int Bm_low_mem = 0; @@ -2920,6 +2927,9 @@ void bm_print_bitmaps() { int bm_release(int handle, int clear_render_targets) { Assert(handle >= 0); + if (!bm_inited) + return 0; + bitmap_entry *be; be = bm_get_entry(handle); @@ -3019,6 +3029,45 @@ int bm_release(int handle, int clear_render_targets) { return 1; } +int bm_add_ref(int handle) { + if (!bm_is_valid(handle)) + return -1; + + // render targets are not counted, because bm_release() will not release them anyway + if (bm_is_render_target(handle)) + return handle; + + // animations are counted on their first frame + int first_frame = bm_get_info(handle); + if (first_frame < 0) + return -1; + + bm_get_entry(first_frame)->load_count++; + + return first_frame; +} + +int bm_release_ref(int handle) { + if (!bm_is_valid(handle)) + return 0; + + // animations are counted on their first frame + int first_frame = bm_get_info(handle); + if (first_frame < 0) + return 0; + + // a locked bitmap can't be freed right now, but our reference can still be given up; the data is then freed by a + // later release (render targets are never counted, see bm_add_ref()) + auto be = bm_get_entry(first_frame); + if (be->ref_count != 0 && !bm_is_render_target(first_frame)) { + if (be->load_count > 0) + be->load_count--; + return 0; + } + + return bm_release(first_frame); +} + bool bm_release_rendertarget(int handle) { Assert(handle >= 0); @@ -3202,7 +3251,7 @@ bool bm_set_render_target(int handle, int face) { return false; } -int bm_unload(int handle, int clear_render_targets, bool nodebug) { +int bm_unload(int handle, int clear_render_targets, bool nodebug, bool keep_reference) { bitmap_entry *be; bitmap *bmp; @@ -3210,6 +3259,9 @@ int bm_unload(int handle, int clear_render_targets, bool nodebug) { return -1; } + if (!bm_inited) + return -1; + be = bm_get_entry(handle); bmp = &be->bm; @@ -3232,7 +3284,7 @@ int bm_unload(int handle, int clear_render_targets, bool nodebug) { // kind of like ref_count except it gets around the lock/unlock usage problem // this gets set for each bm_load() call so we can make sure and not unload it // from memory, even if we *can*, until it's really not needed anymore - if (!Bm_ignore_load_count) { + if (!Bm_ignore_load_count && !keep_reference) { if (be->load_count > 0) be->load_count--; @@ -3242,6 +3294,9 @@ int bm_unload(int handle, int clear_render_targets, bool nodebug) { } } + // freeing the data resets the load count, but paging out gives up no references, so remember it + int saved_load_count = be->load_count; + // be sure that all frames of an ani are unloaded - taylor if (bm_is_anim(be) == true) { int i, first = be->info.ani.first_frame; @@ -3264,9 +3319,24 @@ int bm_unload(int handle, int clear_render_targets, bool nodebug) { bm_free_data(bm_get_slot(handle)); // clears flags, bbp, data, etc } + if (keep_reference) + be->load_count = saved_load_count; + return 1; } +int bm_page_out(int handle) { + if (!bm_is_valid(handle)) + return 0; + + // animations are counted on their first frame + int first_frame = bm_get_info(handle); + if (first_frame < 0) + return 0; + + return (bm_unload(first_frame, 0, false, true) == 1) ? 1 : 0; +} + void bm_unload_all() { // since bm_unload_all() should only be called from game_shutdown() it should be // safe to ignore load_count's and unload anyway diff --git a/code/bmpman/bmpman.h b/code/bmpman/bmpman.h index d52a01f5125..7de1bb915f2 100644 --- a/code/bmpman/bmpman.h +++ b/code/bmpman/bmpman.h @@ -257,11 +257,17 @@ int bm_create_3d(int bpp, int w, int h, int d, void* data = nullptr); * @param handle The index number of the bitmap to free * @param clear_render_targets If true, release a render target * @param nodebug If true, exclude certain debug messages + * @param keep_reference If true, no load-count reference is given up: the data is freed, but every reference to + * the bitmap is preserved, so it is simply reloaded the next time it is locked. For + * animations, pass the first frame, since that is where the references are counted. + * See bm_page_out() * - * @returns 0 if not successful, - * @returns 1 if successful + * @returns 1 if the data was freed, + * @returns 0 if it was not (e.g. the bitmap is locked, or another holder still needs it), or + * @returns -1 if the handle is invalid, bmpman is not initialized, or the bitmap is a render target and + * clear_render_targets is not set */ -int bm_unload(int handle, int clear_render_targets = 0, bool nodebug = false); +int bm_unload(int handle, int clear_render_targets = 0, bool nodebug = false, bool keep_reference = false); /** * @brief Quickly unloads a bitmap's data, ignoring the load_count @@ -276,6 +282,20 @@ int bm_unload(int handle, int clear_render_targets = 0, bool nodebug = false); */ int bm_unload_fast(int handle, int clear_render_targets = 0); +/** + * @brief Frees a bitmap's data without giving up the caller's load-count reference + * + * @details Use this to page out a bitmap that will be needed again later: the slot and every reference to the bitmap are + * kept, and the data is reloaded the next time it is locked. Unlike bm_unload(), this never consumes a reference, so + * it works no matter how many holders the bitmap has. A bitmap that is currently locked is left alone. + * + * @param handle The bitmap handle. For animations, any frame may be passed; the whole animation is paged out. + * + * @returns 1 if the data was freed, + * @returns 0 otherwise + */ +int bm_page_out(int handle); + /** * @brief Frees both a bitmap's data and it's associated slot. * @@ -293,6 +313,36 @@ int bm_unload_fast(int handle, int clear_render_targets = 0); */ int bm_release(int handle, int clear_render_targets = 0); +/** + * @brief Takes an additional load-count reference on a bitmap that is already loaded + * + * @details This is the equivalent of loading the bitmap a second time: the bitmap will not be freed until every + * reference has been released. Use it when storing a handle that was loaded by someone else, so that the stored + * handle stays valid even after the original loader releases it. Every call must be balanced by a call to + * bm_release_ref(). + * + * @param handle The bitmap handle. For animations, any frame may be passed; the reference is taken on the first frame. + * Render targets are not counted, since bm_release() will not release them without being explicitly asked to. + * + * @returns the handle the reference was taken on (the first frame for animations), or the handle itself for a render + * target, on which no reference is taken, or + * @returns -1 if the handle is not a valid bitmap + */ +int bm_add_ref(int handle); + +/** + * @brief Releases a load-count reference taken by bm_add_ref() or by loading the bitmap + * + * @details Same as bm_release(), except that any frame of an animation may be passed, and that if the bitmap is + * currently locked the reference is still given up; the bitmap is then freed by a later release, or at shutdown. + * + * @param handle The bitmap handle. For animations, any frame may be passed; the reference is released on the first frame. + * + * @returns 1 if the bitmap was freed, + * @returns 0 otherwise + */ +int bm_release_ref(int handle); + /** * @brief Detaches the render target of a bitmap if it exists * diff --git a/code/lab/manager/lab_manager.cpp b/code/lab/manager/lab_manager.cpp index 038785750b0..e2afd5cf57c 100644 --- a/code/lab/manager/lab_manager.cpp +++ b/code/lab/manager/lab_manager.cpp @@ -856,22 +856,7 @@ void LabManager::changeShipInternal() { ship_objp->special_exp_damage = -1; // If the ship class defines replacement textures, load them and apply them to the ship - // load the texture - auto replacements = ship_infop->replacement_textures; - for (auto& tr : replacements) { - if (!stricmp(tr.new_texture, "invisible")) - { - // invisible is a special case - tr.new_texture_id = REPLACE_WITH_INVISIBLE; - } - else - { - // try to load texture or anim as normal - tr.new_texture_id = bm_load_either(tr.new_texture); - } - } - - ship_objp->apply_replacement_textures(replacements); + ship_objp->load_and_apply_replacement_textures(ship_infop->replacement_textures); ship_page_in_textures(ship_objp->ship_info_index); if (!ship_infop->default_team_name.empty()) diff --git a/code/menuui/techmenu.cpp b/code/menuui/techmenu.cpp index 407cd8c63db..535f66d3aa0 100644 --- a/code/menuui/techmenu.cpp +++ b/code/menuui/techmenu.cpp @@ -537,10 +537,6 @@ void techroom_ships_render(float frametime) closeup_pos = sip->closeup_pos; closeup_zoom = sip->closeup_zoom; - if (!sip->replacement_textures.empty()) { - render_info.set_replacement_textures(Techroom_modelnum, sip->replacement_textures); - } - if (sip->flags[Ship::Info_Flags::No_lighting]) noLighting = true; @@ -600,13 +596,7 @@ void techroom_ships_render(float frametime) render_info.set_detail_level_lock(0); int model_instance = -1; - auto cache_result = model_get_cached_ui_render_instance(Techroom_modelnum, &model_instance); - // Only set up the instance when it was freshly created; the cached instance persists across - // frames, so re-running this every frame would re-apply initial animations on top of the - // already-animated pose and make animated submodels (e.g. turrets) flip/jitter. - if (Tab == SHIPS_DATA_TAB && cache_result == TriStateBool::TRUE_) { - model_set_up_techroom_instance(&Ship_info[Cur_entry_index], model_instance); - } + model_get_cached_ui_render_instance_for_class(render_info, Techroom_modelnum, (Tab == SHIPS_DATA_TAB) ? &Ship_info[Cur_entry_index] : nullptr, &model_instance); if(shadow_maybe_start_frame(Shadow_disable_overrides.disable_techroom)) { diff --git a/code/mission/missionparse.cpp b/code/mission/missionparse.cpp index 2769cb4c5b6..1a40c86d12d 100644 --- a/code/mission/missionparse.cpp +++ b/code/mission/missionparse.cpp @@ -5561,6 +5561,41 @@ void post_process_mission_props() } } +// Loads the bitmaps for a parse object's replacement textures. The parse object holds a reference to each one for the +// lifetime of the mission, so that every ship created from it (e.g. each wave of a wing) can share them. +static void mission_load_replacement_textures(p_object &p_obj) +{ + for (auto &tr : p_obj.replacement_textures) + { + if (!stricmp(tr.new_texture, "invisible")) + { + // invisible is a special case + tr.new_texture_id = REPLACE_WITH_INVISIBLE; + } + else + { + // try to load texture or anim as normal + tr.new_texture_id = bm_load_either(tr.new_texture); + } + + // not found? + if (tr.new_texture_id == -1) + mprintf(("Could not load replacement texture %s for ship %s\n", tr.new_texture, p_obj.name)); + } +} + +// Releases the references taken by mission_load_replacement_textures() +static void mission_release_replacement_textures(p_object &p_obj) +{ + for (auto &tr : p_obj.replacement_textures) + { + // the check skips REPLACE_WITH_INVISIBLE + if (tr.new_texture_id >= 0) + bm_release_ref(tr.new_texture_id); + tr.new_texture_id = -1; + } +} + // Goober5000 void post_process_ships_wings() { @@ -5628,28 +5663,14 @@ void post_process_ships_wings() } // also load any replacement textures (do this outside the parse loop because we may have ship class replacements too) - for (SCP_vector::iterator tr = p_obj.replacement_textures.begin(); tr != p_obj.replacement_textures.end(); ++tr) - { - // load the texture - if (!stricmp(tr->new_texture, "invisible")) - { - // invisible is a special case - tr->new_texture_id = REPLACE_WITH_INVISIBLE; - } - else - { - // try to load texture or anim as normal - tr->new_texture_id = bm_load_either(tr->new_texture); - } - - // not found? - if (tr->new_texture_id < 0) - mprintf(("Could not load replacement texture %s for ship %s\n", tr->new_texture, p_obj.name)); + mission_load_replacement_textures(p_obj); - // account for FRED - if (Fred_running) + // account for FRED + if (Fred_running) + { + for (const auto &tr : p_obj.replacement_textures) { - Fred_texture_replacements.push_back(*tr); + Fred_texture_replacements.push_back(tr); Fred_texture_replacements.back().new_texture_id = -1; } } @@ -7399,6 +7420,24 @@ void support_ship_info::reset() } } +// Releases the references to the replacement textures of every parse object (see mission_load_replacement_textures()); +// ships created from those objects hold their own. +static void mission_release_replacement_textures() +{ + for (auto &p_obj : Parse_objects) + mission_release_replacement_textures(p_obj); + + mission_release_replacement_textures(Support_ship_pobj); +} + +void mission_parse_level_close() +{ + // The parse objects themselves are kept until the next mission is parsed, since the debriefing and red alert code + // still consult them, but nothing needs their replacement textures once the mission's ships are gone. (This is + // also done in mission_init(), for the paths that never reach a level close, such as loading a mission in FRED.) + mission_release_replacement_textures(); +} + /** * Initialize the mission and related data structures. */ @@ -7459,6 +7498,7 @@ void mission_init(mission *pm, bool quick_init) } Total_initially_docked = 0; + mission_release_replacement_textures(); Parse_objects.clear(); list_init(&Ship_arrival_list); // init list for arrival ships @@ -7577,6 +7617,7 @@ void mission_parse_close() } // the destructor for each p_object will clear its dock list + mission_release_replacement_textures(); Parse_objects.clear(); } @@ -9248,6 +9289,9 @@ void mission_bring_in_support_ship( object *requester_objp ) // object since I'm no longer working with a mission file. These exceptions will be noted with // comments + // the previous support ship's parse object may still hold replacement texture references + mission_release_replacement_textures(Support_ship_pobj); + Support_ship_pobj = p_object(); // get a fresh p_object with default fields Arriving_support_ship = &Support_ship_pobj; pobj = Arriving_support_ship; @@ -9263,6 +9307,7 @@ void mission_bring_in_support_ship( object *requester_objp ) Assert(pobj->ship_max_hull_strength > 0.0f); // Goober5000: div-0 check (not shield because we might not have one) pobj->max_shield_recharge = sip->max_shield_recharge; pobj->replacement_textures = sip->replacement_textures; // initialize our set with the ship class set, which may be empty + mission_load_replacement_textures(*pobj); pobj->score = sip->score; // get average position of all ships diff --git a/code/mission/missionparse.h b/code/mission/missionparse.h index bc6765b6b4f..4e00fc25c55 100644 --- a/code/mission/missionparse.h +++ b/code/mission/missionparse.h @@ -625,6 +625,9 @@ void resolve_parse_flags(object *objp, flagset &par void mission_parse_close(); +// Frees the mission parsing resources that are only needed while a mission is being played; called from game_level_close() +void mission_parse_level_close(); + // used in fred management.cpp when creating a new mission void apply_default_custom_data(mission* pm); diff --git a/code/missionui/missionbrief.cpp b/code/missionui/missionbrief.cpp index 7f683cfad15..3a385eba19f 100644 --- a/code/missionui/missionbrief.cpp +++ b/code/missionui/missionbrief.cpp @@ -706,15 +706,25 @@ brief_icon *brief_get_closeup_icon() return Closeup_icon; } +// deletes the model instance an icon was set up with for a closeup, if any +static void brief_delete_closeup_instance(brief_icon *bi) +{ + if (bi == nullptr) + return; + + // the instance may already be gone, e.g. if all models were freed in the meantime + if (model_instance_exists(bi->model_instance_num)) + model_delete_instance(bi->model_instance_num); + + bi->model_instance_num = -1; +} + // stop showing the closeup view of an icon void brief_turn_off_closeup_icon(bool api_access) { // turn off closeup if ( Closeup_icon != NULL ) { - if (Closeup_icon->model_instance_num >= 0) { - model_delete_instance(Closeup_icon->model_instance_num); - Closeup_icon->model_instance_num = -1; - } + brief_delete_closeup_instance(Closeup_icon); if (!api_access) { gamesnd_play_iface(InterfaceSounds::BRIEF_ICON_SELECT); @@ -768,11 +778,13 @@ void brief_set_default_closeup() bs = &Briefing->stages[0]; if ( Briefing->num_stages <= 0 ) { + brief_delete_closeup_instance(Closeup_icon); Closeup_icon = NULL; return; } if ( bs->num_icons <= 0 ) { + brief_delete_closeup_instance(Closeup_icon); Closeup_icon = NULL; return; } @@ -1105,9 +1117,9 @@ void brief_render_closeup(int ship_class, float frametime) gr_set_clip(Closeup_region[gr_screen.res][0], Closeup_region[gr_screen.res][1], w, h, GR_RESIZE_MENU); } - auto sip = &Ship_info[ship_class]; - if (!sip->replacement_textures.empty()) - render_info.set_replacement_textures(Closeup_icon->modelnum, sip->replacement_textures); + // the instance carries the ship class's replacement textures, if any + if (Closeup_icon->model_instance_num >= 0) + render_info.set_replacement_textures(model_get_instance(Closeup_icon->model_instance_num)->texture_replace); render_info.set_flags(MR_AUTOCENTER); } @@ -1277,6 +1289,9 @@ int brief_setup_closeup(brief_icon *bi, bool api_access) ship_info *sip=NULL; vec3d tvec; + // only the current closeup icon should own a model instance, so let go of the previous one's before switching + brief_delete_closeup_instance(Closeup_icon); + Closeup_icon = bi; Closeup_icon->modelnum = -1; Closeup_icon->model_instance_num = -1; @@ -1425,6 +1440,7 @@ void brief_update_closeup_icon(int mode) brief_setup_closeup(bi); } else { + brief_delete_closeup_instance(Closeup_icon); Closeup_icon = NULL; } } @@ -1942,6 +1958,15 @@ void brief_close(bool api_access) // unload the bitmaps brief_unload_bitmaps(); + // delete the model instances of every icon that was set up for a closeup, in case any were orphaned + if (Briefing != nullptr) { + for (int i = 0; i < Briefing->num_stages; i++) { + brief_stage *bs = &Briefing->stages[i]; + for (int j = 0; j < bs->num_icons; j++) + brief_delete_closeup_instance(&bs->icons[j]); + } + } + brief_common_close(); } diff --git a/code/missionui/missionscreencommon.cpp b/code/missionui/missionscreencommon.cpp index d2685e9da5c..15b2b0a21c7 100644 --- a/code/missionui/missionscreencommon.cpp +++ b/code/missionui/missionscreencommon.cpp @@ -1657,19 +1657,10 @@ void draw_model_icon(int model_id, uint64_t flags, int x, int y, int w, int h, s common_setup_room_lights(); } - if (sip != NULL && sip->replacement_textures.size() > 0) - { - render_info.set_replacement_textures(model_id, sip->replacement_textures); - } - Glowpoint_override = true; model_clear_instance(model_id); int model_instance = -1; - auto cache_result = model_get_cached_ui_render_instance(model_id, &model_instance); - // Only set up the instance when it was freshly created; the cached instance persists across frames. - if (sip != nullptr && cache_result == TriStateBool::TRUE_) { - model_set_up_techroom_instance(sip, model_instance); - } + model_get_cached_ui_render_instance_for_class(render_info, model_id, sip, &model_instance); render_info.set_flags(flags); model_render_immediate(&render_info, model_id, model_instance, &object_orient, &vmd_zero_vector); @@ -1688,12 +1679,10 @@ void draw_model_rotating(model_render_params *render_info, int ship_class, int m if (model_id < 0) return; + bool is_ship = !(flags & MR_IS_MISSILE) && Ship_info.in_bounds(ship_class); + int model_instance = -1; - auto cache_result = model_get_cached_ui_render_instance(model_id, &model_instance); - // Only set up the instance when it was freshly created; the cached instance persists across frames. - if (!(flags & MR_IS_MISSILE) && SCP_vector_inbounds(Ship_info, ship_class) && cache_result == TriStateBool::TRUE_) { - model_set_up_techroom_instance(&Ship_info[ship_class], model_instance); - } + model_get_cached_ui_render_instance_for_class(*render_info, model_id, is_ship ? &Ship_info[ship_class] : nullptr, &model_instance); lighting_profiles::set_non_mission_profile non_mission_lighting_profile; diff --git a/code/missionui/missionshipchoice.cpp b/code/missionui/missionshipchoice.cpp index dde6ffc8ba9..491b5d09441 100644 --- a/code/missionui/missionshipchoice.cpp +++ b/code/missionui/missionshipchoice.cpp @@ -1359,11 +1359,6 @@ void ship_select_do(float frametime) render_info.set_team_color(sip->default_team_name, "none", 0, 0); } - if (sip->replacement_textures.size() > 0) - { - render_info.set_replacement_textures(ShipSelectModelNum, sip->replacement_textures); - } - select_effect_params params; params.effect = sip->selection_effect; params.fs2_grid_color = sip->fs2_effect_grid_color; diff --git a/code/missionui/missionweaponchoice.cpp b/code/missionui/missionweaponchoice.cpp index 229c68ed68d..3bf0333f2a4 100644 --- a/code/missionui/missionweaponchoice.cpp +++ b/code/missionui/missionweaponchoice.cpp @@ -842,17 +842,9 @@ void draw_3d_overhead_view(int model_num, model_clear_instance(model_num); int model_instance = -1; - auto cache_result = model_get_cached_ui_render_instance(model_num, &model_instance); - // Only set up the instance when it was freshly created; the cached instance persists across frames. - if (cache_result == TriStateBool::TRUE_) { - model_set_up_techroom_instance(sip, model_instance); - } + model_get_cached_ui_render_instance_for_class(render_info, model_num, sip, &model_instance); polymodel* pm = model_get(model_num); - if (sip->replacement_textures.size() > 0) { - render_info.set_replacement_textures(model_num, sip->replacement_textures); - } - if (shadow_maybe_start_frame(Shadow_disable_overrides.disable_mission_select_weapons)) { gr_reset_clip(); shadows_start_render(&vmd_identity_matrix, diff --git a/code/model/model.h b/code/model/model.h index 012e5b4bf4f..097c4c7ec4d 100644 --- a/code/model/model.h +++ b/code/model/model.h @@ -163,8 +163,10 @@ struct submodel_instance #define TM_SPEC_GLOSS_TYPE 6 // optional reflectance map (specular and gloss) #define TM_AMBIENT_TYPE 7 // optional ambient occlusion map with ambient occlusion and cavity occlusion factors for red and green channels. #define TM_NUM_TYPES 8 //WMC - Number of texture_info objects in texture_map - //Used by scripting - if you change this, do a search - //to update switch() statement in lua.cpp + //Used by scripting: the "textures" and "modelinstancetextures" Lua handles expose + //a flat array with TM_NUM_TYPES entries per texture_map, in the order listed above. + //If you add or reorder a type, update the docs for those handles in + //scripting/api/objs/model.cpp and scripting/api/objs/modelinstance.cpp. inline const SCP_map MODEL_TEXTURE_SUFFIXES = { { TM_GLOW_TYPE, "-glow" }, @@ -191,12 +193,35 @@ inline const SCP_string& model_texture_longest_suffix() { // Goober5000 - since we need something < 0 #define REPLACE_WITH_INVISIBLE -47 -class model_texture_replace : public std::array { +// Per-instance texture replacements, indexed by (texture_map index * TM_NUM_TYPES + TM_*_TYPE). Each entry is a bitmap +// handle, -1 for no replacement, or REPLACE_WITH_INVISIBLE. The array owns one load-count reference on every bitmap +// handle it holds and releases them all when it is destroyed, so entries can only be written through adopt(), +// reference(), or clear(). The exception is render targets, which bm_release() will not free unless explicitly asked; +// the cockpit display code stores those here, and they are left alone when the array is destroyed, as before. +class model_texture_replace +{ + std::array m_handles; + public: - model_texture_replace() : std::array() { - for (int& tex : *this) - tex = -1; - } + model_texture_replace(); + ~model_texture_replace(); + + model_texture_replace(const model_texture_replace&) = delete; + model_texture_replace& operator=(const model_texture_replace&) = delete; + + int operator[](int index) const { return m_handles[index]; } + auto begin() const { return m_handles.begin(); } + auto end() const { return m_handles.end(); } + + // Stores a handle whose load-count reference (e.g. from bm_load) the array takes over from the caller. + void adopt(int index, int handle); + + // Stores a handle that someone else owns; the array takes its own load-count reference on it. REPLACE_WITH_INVISIBLE is + // stored as-is; any other invalid handle clears the slot. + void reference(int index, int handle); + + // Removes any replacement in this slot. + void clear(int index); }; // Data specific to a particular instance of a model. @@ -789,6 +814,7 @@ class texture_info private: int original_texture; // what gets read in from file int texture; // what texture you draw with; reset to original_textures by model_set_instance + int held_texture; // texture (if any) on which this object holds its own load-count reference; see SetTexture //WMC - Removed unneeded struct and is_anim to clean this up. //If num_frames is < 2, it doesn't need to be treated like an animation. @@ -810,8 +836,14 @@ class texture_info void PageIn(); void PageOut(bool release); + // Resets the texture to the original one, releasing any reference taken by SetTexture(..., true). int ResetTexture(); - int SetTexture(int n_tex); + + // Sets the texture to draw with. If take_reference is true, this object takes its own load-count reference on + // the new texture and keeps it until ResetTexture(), PageOut(true), or a later SetTexture() with take_reference + // releases it. Use this when the handle comes from somewhere the model does not control, such as a script, so + // that the texture cannot be freed out from under the model. + int SetTexture(int n_tex, bool take_reference = false); }; // taylor @@ -1267,6 +1299,15 @@ extern void model_set_submodel_instance_motion_info(bsp_info *sm, submodel_insta // Sets the submodel instance data in a submodel extern void model_set_up_techroom_instance(ship_info *sip, int model_instance_num); +// Returns whether a model instance with this number currently exists. Use this before acting on an instance number that +// was stored earlier, since instances can be deleted (and their numbers reused) at any time. +extern bool model_instance_exists(int model_instance_num); + +// Loads the given replacement textures (e.g. a ship class's table entry, which has filenames only) for a model instance, +// discarding any replacement textures the instance already had. The instance owns the loaded bitmaps. +struct texture_replace; +extern void model_instance_load_replacement_textures(polymodel_instance *pmi, const SCP_vector &replacements); + void model_replicate_submodel_instance(polymodel *pm, polymodel_instance *pmi, int submodel_num, flagset& flags); // Adds an electrical arcing effect to a submodel diff --git a/code/model/modelinterp.cpp b/code/model/modelinterp.cpp index 57b91235959..d8610e10545 100644 --- a/code/model/modelinterp.cpp +++ b/code/model/modelinterp.cpp @@ -1454,29 +1454,18 @@ void model_page_out_textures(polymodel* pm, bool release, const SCP_set& sk pm->maps[i].PageOut(release); } - // NOTE: "release" doesn't work here for some, as of yet unknown, reason - taylor + // NOTE: these can only be paged out, not released, because a virtual POF copies glow point banks from its source + // models without taking a reference per copy, so two banks can share one handle and releasing would over-release. for (j = 0; j < pm->n_glow_point_banks; j++) { if(skipGlowBanks.contains(j)) continue; glow_point_bank* bank = &pm->glow_point_banks[j]; - if (bank->glow_bitmap >= 0) { - // if (release) { - // bm_release(bank->glow_bitmap); - // bank->glow_bitmap = -1; - // } else { - bm_unload(bank->glow_bitmap); - // } - } + if (bank->glow_bitmap >= 0) + bm_page_out(bank->glow_bitmap); - if (bank->glow_neb_bitmap >= 0) { - // if (release) { - // bm_release(bank->glow_neb_bitmap); - // bank->glow_neb_bitmap = -1; - // } else { - bm_unload(bank->glow_neb_bitmap); - // } - } + if (bank->glow_neb_bitmap >= 0) + bm_page_out(bank->glow_neb_bitmap); } } @@ -2670,18 +2659,17 @@ texture_info::texture_info() } texture_info::texture_info(int bm_handle) { + clear(); + if(!bm_is_valid(bm_handle)) - { - clear(); return; - } this->original_texture = bm_handle; this->ResetTexture(); } void texture_info::clear() { - texture = original_texture = -1; + texture = original_texture = held_texture = -1; num_frames = 0; total_time = 1.0f; } @@ -2722,26 +2710,48 @@ void texture_info::PageIn() void texture_info::PageOut(bool release) { - if (texture >= 0) { - if (release) { - bm_release(texture); - texture = -1; - num_frames = 0; - total_time = 1.0f; - } else { - bm_unload(texture); - } + if (release) { + // release our own reference to a texture that was set from outside, if any + if (held_texture >= 0) + bm_release_ref(held_texture); + + // release the texture we loaded; note that this is not necessarily the texture currently being drawn with + if (original_texture >= 0) + bm_release_ref(original_texture); + + clear(); + } else if (original_texture >= 0) { + // page out the texture we loaded, keeping our reference so that it can be paged back in; a texture set from + // outside is left alone, since whoever set it may still be using it + bm_page_out(original_texture); } } int texture_info::ResetTexture() { - return this->SetTexture(original_texture); + int result = this->SetTexture(original_texture); + + // drop the held reference once we are back on the original + if (held_texture >= 0 && texture == original_texture) { + bm_release_ref(held_texture); + held_texture = -1; + } + + return result; } -int texture_info::SetTexture(int n_tex) +int texture_info::SetTexture(int n_tex, bool take_reference) { if(n_tex != -1 && !bm_is_valid(n_tex)) return texture; + if (take_reference) { + // take the new reference before releasing the old one, in case they are the same texture + if (n_tex != -1) + bm_add_ref(n_tex); + if (held_texture >= 0) + bm_release_ref(held_texture); + held_texture = n_tex; + } + //Set the new texture texture = n_tex; @@ -2821,6 +2831,56 @@ void texture_map::ResetToOriginal() this->textures[i].ResetTexture(); } +//********************-----CLASS: model_texture_replace-----********************// +model_texture_replace::model_texture_replace() +{ + m_handles.fill(-1); +} + +model_texture_replace::~model_texture_replace() +{ + for (int tex : m_handles) + if (tex >= 0) + bm_release_ref(tex); +} + +static bool valid_replacement_index(int index) +{ + Assertion(index >= 0 && index < MAX_REPLACEMENT_TEXTURES, "Replacement texture index %d is out of range!", index); + return index >= 0 && index < MAX_REPLACEMENT_TEXTURES; +} + +void model_texture_replace::adopt(int index, int handle) +{ + if (!valid_replacement_index(index)) + return; + + int& slot = m_handles[index]; + + if (slot >= 0) + bm_release_ref(slot); + + slot = handle; +} + +void model_texture_replace::reference(int index, int handle) +{ + // check this before taking the reference, since adopt() would refuse it without releasing + if (!valid_replacement_index(index)) + return; + + // REPLACE_WITH_INVISIBLE is stored as-is; anything else must be a valid bitmap, or the slot is cleared + if (handle != REPLACE_WITH_INVISIBLE && bm_add_ref(handle) < 0) + handle = -1; + + adopt(index, handle); +} + +void model_texture_replace::clear(int index) +{ + adopt(index, -1); +} + bsp_polygon_data::bsp_polygon_data(ubyte* _bsp_data, int _bsp_data_size) { Polygon_vertices.clear(); diff --git a/code/model/modelread.cpp b/code/model/modelread.cpp index 56194ca5886..ed6d02abf0c 100644 --- a/code/model/modelread.cpp +++ b/code/model/modelread.cpp @@ -2415,7 +2415,9 @@ modelread_status read_model_file_no_subsys(polymodel * pm, const char* filename, if (bank->glow_neb_bitmap < 0) { - bank->glow_neb_bitmap = bank->glow_bitmap; + // leave it invalid; the renderer falls back to the normal glowpoint texture. (Aliasing the + // two handles here would mean one bitmap owned by two fields, which the paging code can't tell + // apart from two separate textures.) nprintf(( "Model", "Glow point bank nebula texture not found for '%s', using normal glowpoint texture instead\n", pm->filename)); // Error( LOCATION, "Couldn't open texture '%s'\nreferenced by model '%s'\n", glow_texture_name, pm->filename ); } @@ -3841,6 +3843,11 @@ int num_model_instances() return static_cast(Polygon_model_instances.size()); } +bool model_instance_exists(int model_instance_num) +{ + return (model_instance_num >= 0) && (model_instance_num < num_model_instances()) && (Polygon_model_instances[model_instance_num] != nullptr); +} + polymodel_instance* model_get_instance(int model_instance_num) { Assert( model_instance_num >= 0 ); @@ -5018,6 +5025,50 @@ void model_set_submodel_instance_motion_info(bsp_info *sm, submodel_instance *sm smi->shift_accel = sm->default_shift_accel; } +void model_instance_load_replacement_textures(polymodel_instance *pmi, const SCP_vector &replacements) +{ + // discard whatever the instance had, because the new positions may be different + pmi->texture_replace = nullptr; + + if (replacements.empty()) + return; + + auto pm = model_get(pmi->model_num); + pmi->texture_replace = std::make_shared(); + + // now fill them in according to texture name + for (const auto &tr : replacements) + { + int new_tex = -1; + + // look for textures + for (int j = 0; j < pm->n_textures; j++) + { + int tnum = pm->maps[j].FindTexture(tr.old_texture); + if (tnum < 0) + continue; + + // load the replacement the first time it is actually needed, and only once; each slot takes its own reference + if (new_tex == -1) + { + if (!stricmp(tr.new_texture, "invisible")) + new_tex = REPLACE_WITH_INVISIBLE; + else + new_tex = bm_load_either(tr.new_texture); + + if (new_tex == -1) + break; + } + + pmi->texture_replace->reference(j * TM_NUM_TYPES + tnum, new_tex); + } + + // and the reference from loading is no longer needed (the check skips REPLACE_WITH_INVISIBLE) + if (new_tex >= 0) + bm_release_ref(new_tex); + } +} + // Sets the submodel instance data when a tech room model instance is created. // This only needs to be done at creation, not every frame. void model_set_up_techroom_instance(ship_info *sip, int model_instance_num) @@ -5026,6 +5077,9 @@ void model_set_up_techroom_instance(ship_info *sip, int model_instance_num) auto pm = model_get(pmi->model_num); flagset empty; + // the instance carries the class's replacement textures, so that screens rendering it don't have to load them themselves + model_instance_load_replacement_textures(pmi, sip->replacement_textures); + sip->animations.clearShipData(pmi); sip->animations.getAll(pmi, animation::ModelAnimationTriggerType::Initial).start(animation::ModelAnimationDirection::FWD, true, true); diff --git a/code/model/modelrender.cpp b/code/model/modelrender.cpp index 63c978a3f24..3bf5912fe81 100644 --- a/code/model/modelrender.cpp +++ b/code/model/modelrender.cpp @@ -88,7 +88,7 @@ size_t model_hash_subsystem_name_list_for_cache(const SCP_vector& su // Returns TriStateBool::TRUE_ if a new instance was created, TriStateBool::FALSE_ if an existing instance was returned, // or TriStateBool::UNKNOWN_ if there was an error (and model_instance_out will be set to -1 in this case) -TriStateBool model_get_cached_ui_render_instance(int model_num, int* model_instance_out, size_t instance_data_hash) +static TriStateBool model_get_cached_ui_render_instance(int model_num, int* model_instance_out, size_t instance_data_hash = 0) { Assertion(model_instance_out != nullptr, "model_instance_out must not be null!"); if (model_instance_out == nullptr) { @@ -120,6 +120,29 @@ TriStateBool model_get_cached_ui_render_instance(int model_num, int* model_insta return created_new ? TriStateBool::TRUE_ : TriStateBool::FALSE_; } +TriStateBool model_get_cached_ui_render_instance_for_class(model_render_params& render_info, int model_num, ship_info* sip, int* model_instance_out, size_t instance_data_hash) +{ + // key the instance by ship class as well, since classes sharing a model may differ in replacement textures + if (sip != nullptr) + boost::hash_combine(instance_data_hash, static_cast(sip - Ship_info.data())); + + auto cache_result = model_get_cached_ui_render_instance(model_num, model_instance_out, instance_data_hash); + if (cache_result == TriStateBool::UNKNOWN_) + return cache_result; + + // only set up the instance when it was freshly created; the cached instance persists across frames, so re-running + // this every frame would re-apply initial animations on top of the already-animated pose + if (sip != nullptr && cache_result == TriStateBool::TRUE_) + model_set_up_techroom_instance(sip, *model_instance_out); + + // the instance carries the ship class's replacement textures, if any; a bare model has none, and the caller may + // have set some of its own + if (sip != nullptr) + render_info.set_replacement_textures(model_get_instance(*model_instance_out)->texture_replace); + + return cache_result; +} + void model_process_cached_ui_render_instances() { if (Ui_render_instance_cache_last_processed_framecount == Framecount) { @@ -330,27 +353,6 @@ void model_render_params::set_replacement_textures(std::shared_ptr& replacement_textures) -{ - auto textures = std::make_shared(); - - polymodel* pm = model_get(modelnum); - - for (const auto& tr : replacement_textures) - { - for (int i = 0; i < pm->n_textures; ++i) - { - texture_map *tmap = &pm->maps[i]; - - int tnum = tmap->FindTexture(tr.old_texture); - if (tnum > -1) - (*textures)[i * TM_NUM_TYPES + tnum] = bm_load(tr.new_texture); - } - } - - Replacement_textures = std::move(textures); -} - void model_render_params::set_insignia_bitmap(int bitmap) { Insignia_bitmap = bitmap; @@ -1774,7 +1776,7 @@ void model_render_glowpoint_bitmap(int point_num, const vec3d *pos, const matrix vm_vec_sub(&tempv,&View_position,&loc_offset); vm_vec_normalize(&tempv); - if ( The_mission.flags[Mission::Mission_Flags::Fullneb] ) { + if ( The_mission.flags[Mission::Mission_Flags::Fullneb] && bank->glow_neb_bitmap >= 0 ) { batching_add_quad(bank->glow_neb_bitmap, verts); } else { batching_add_quad(bank->glow_bitmap, verts); @@ -3115,7 +3117,7 @@ void modelinstance_replace_active_texture(polymodel_instance* pmi, const char* o pmi->texture_replace = std::make_shared(); } - (*pmi->texture_replace)[final_index] = texture; + pmi->texture_replace->adopt(final_index, texture); } else Warning(LOCATION, "Invalid texture '%s' used for replacement texture", old_name); } @@ -3152,7 +3154,6 @@ bool render_tech_model(tech_render_type model_type, int x1, int y1, int x2, int // Make sure model is loaded model_num = model_load(sip, true); - render_info.set_replacement_textures(model_num, sip->replacement_textures); break; @@ -3240,29 +3241,26 @@ bool render_tech_model(tech_render_type model_type, int x1, int y1, int x2, int if (model_type == TECH_SHIP) { auto sip = &Ship_info[class_idx]; const auto subsystem_hash = model_hash_subsystem_name_list_for_cache(destroyed_subsystems); - const auto cache_result = model_get_cached_ui_render_instance(model_num, &model_instance, subsystem_hash); - if (cache_result == TriStateBool::TRUE_) { - model_set_up_techroom_instance(sip, model_instance); - if (!destroyed_subsystems.empty()) { - auto pm = model_get(model_num); - auto pmi = model_get_instance(model_instance); - flagset empty; - - for (int idx = 0; idx < sip->n_subsystems; ++idx) { - auto& subsystem = sip->subsystems[idx]; - - if (subsystem.subobj_num < 0 || subsystem.subobj_num >= pm->n_models || - subsystem.model_num != model_num) { - continue; - } + const auto cache_result = model_get_cached_ui_render_instance_for_class(render_info, model_num, sip, &model_instance, subsystem_hash); + if (cache_result == TriStateBool::TRUE_ && !destroyed_subsystems.empty()) { + auto pm = model_get(model_num); + auto pmi = model_get_instance(model_instance); + flagset empty; - for (auto& destroyed_name : destroyed_subsystems) { - if (!stricmp(subsystem.subobj_name, destroyed_name.c_str()) || - !stricmp(subsystem.name, destroyed_name.c_str())) { - pmi->submodel[subsystem.subobj_num].blown_off = true; - model_replicate_submodel_instance(pm, pmi, subsystem.subobj_num, empty); - break; - } + for (int idx = 0; idx < sip->n_subsystems; ++idx) { + auto& subsystem = sip->subsystems[idx]; + + if (subsystem.subobj_num < 0 || subsystem.subobj_num >= pm->n_models || + subsystem.model_num != model_num) { + continue; + } + + for (auto& destroyed_name : destroyed_subsystems) { + if (!stricmp(subsystem.subobj_name, destroyed_name.c_str()) || + !stricmp(subsystem.name, destroyed_name.c_str())) { + pmi->submodel[subsystem.subobj_num].blown_off = true; + model_replicate_submodel_instance(pm, pmi, subsystem.subobj_num, empty); + break; } } } diff --git a/code/model/modelrender.h b/code/model/modelrender.h index 6adff8bb9d7..cd2bf25666a 100644 --- a/code/model/modelrender.h +++ b/code/model/modelrender.h @@ -127,7 +127,6 @@ class model_render_params void set_forced_bitmap(int bitmap); void set_insignia_bitmap(int bitmap); void set_replacement_textures(std::shared_ptr textures); - void set_replacement_textures(int modelnum, const SCP_vector& replacement_textures); void set_team_color(const team_color &clr); void set_team_color(const SCP_string &team, const SCP_string &secondaryteam, fix timestamp, int fadetime); void set_clip_plane(const vec3d &pos, const vec3d &normal); @@ -317,7 +316,10 @@ int model_render_determine_detail(float depth, int model_num, int detail_level_l bool render_tech_model(tech_render_type model_type, int x1, int y1, int x2, int y2, float zoom, bool lighting, int class_idx, const matrix* orient, const SCP_string& pof_filename = "", float closeup_zoom = 0, const vec3d* closeup_pos = &vmd_zero_vector, const SCP_string& tcolor = "", const SCP_vector& destroyed_subsystems = SCP_vector()); size_t model_hash_subsystem_name_list_for_cache(const SCP_vector& subsystem_names); -TriStateBool model_get_cached_ui_render_instance(int model_num, int* model_instance_out, size_t instance_data_hash = 0); +// Fetches the cached model instance used to render a ship class (or a bare model, if sip is null) without an object: the +// cache key includes the class, the instance is set up for the class when it is freshly created, and render_info is +// pointed at the instance's replacement textures. +TriStateBool model_get_cached_ui_render_instance_for_class(model_render_params& render_info, int model_num, ship_info* sip, int* model_instance_out, size_t instance_data_hash = 0); void model_clear_cached_ui_render_instances(); void model_process_cached_ui_render_instances(); diff --git a/code/scpui/RocketRenderingInterface.cpp b/code/scpui/RocketRenderingInterface.cpp index 486ab3094b1..dfb68369b55 100644 --- a/code/scpui/RocketRenderingInterface.cpp +++ b/code/scpui/RocketRenderingInterface.cpp @@ -26,8 +26,7 @@ #include "mod_table/mod_table.h" #include "tracing/categories.h" #include "tracing/tracing.h" -#define BMPMAN_INTERNAL -#include "bmpman/bm_internal.h" +#include "bmpman/bmpman.h" using namespace Rocket::Core; @@ -169,12 +168,9 @@ bool RocketRenderingInterface::LoadTexture(TextureHandle& texture_handle, Vector else if (submode.Find("bmpman,") == 0) { int handle = std::atoi(submode.Substring(7).CString()); - auto* entry = bm_get_entry(handle); - if (entry->handle != handle) + if (bm_add_ref(handle) < 0) return false; - entry->load_count++; - bm_get_info(handle, &texture_dimensions.x, &texture_dimensions.y); std::unique_ptr tex(new Texture()); @@ -261,7 +257,8 @@ void RocketRenderingInterface::ReleaseTexture(TextureHandle texture) if (tex->is_animation) { generic_anim_unload(&tex->animation); } else { - bm_release(tex->bm_handle); + // the reference was taken with bm_add_ref(), which counts on the first frame of an animation + bm_release_ref(tex->bm_handle); } delete tex; } diff --git a/code/scripting/api/libs/graphics.cpp b/code/scripting/api/libs/graphics.cpp index 024ea9c009f..3fc154afe44 100644 --- a/code/scripting/api/libs/graphics.cpp +++ b/code/scripting/api/libs/graphics.cpp @@ -1676,8 +1676,9 @@ ADE_FUNC(createTexture, l_Graphics, "[number Width=512, number Height=512, enume ADE_FUNC(loadTexture, l_Graphics, "string Filename, [boolean LoadIfAnimation, boolean NoDropFrames]", "Gets a handle to a texture. If second argument is set to true, animations will also be loaded." "If third argument is set to true, every other animation frame will not be loaded if system has less than 48 MB memory." - "
IMPORTANT: Textures will not be unload themselves unless you explicitly tell them to do so." - "When you are done with a texture, call the unload() function to free up memory.", + "
The texture stays loaded for as long as any Lua reference to the handle exists; its reference is released automatically once the handle is garbage collected. " + "Anything the texture is assigned to, such as a model or ship texture slot, takes its own reference, so the script does not need to keep the handle alive after assigning it. " + "Call unload() to release the texture sooner.", "texture", "Texture handle, or invalid texture handle if texture couldn't be loaded") { diff --git a/code/scripting/api/objs/model.cpp b/code/scripting/api/objs/model.cpp index 319105871e4..7e2e7914b78 100644 --- a/code/scripting/api/objs/model.cpp +++ b/code/scripting/api/objs/model.cpp @@ -590,9 +590,9 @@ ADE_INDEXER(l_ModelSubmodels, "submodel", "number|string IndexOrName", "submodel //**********HANDLE: modeltextures -ADE_OBJ(l_ModelTextures, model_h, "textures", "Array of textures"); +ADE_OBJ(l_ModelTextures, model_h, "textures", "Flat array of model textures. Each material (texture_map) on the model contributes " SCP_TOKEN_TO_STR(TM_NUM_TYPES) " consecutive entries, in this order: base, glow, specular, normal, height, misc, reflectance, ambient occlusion. So for material N (1-based), the base map is at index (N-1)*" SCP_TOKEN_TO_STR(TM_NUM_TYPES) "+1, the glow map at (N-1)*" SCP_TOKEN_TO_STR(TM_NUM_TYPES) "+2, and so on. Slots that the material does not use hold invalid texture handles."); -ADE_FUNC(__len, l_ModelTextures, NULL, "Number of textures on model", "number", "Number of model textures") +ADE_FUNC(__len, l_ModelTextures, nullptr, "Number of texture slots on the model, i.e. the number of materials multiplied by " SCP_TOKEN_TO_STR(TM_NUM_TYPES), "number", "Number of texture slots, or 0 if handle is invalid") { model_h *mth; if (!ade_get_args(L, "o", l_ModelTextures.GetPtr(&mth))) @@ -605,7 +605,7 @@ ADE_FUNC(__len, l_ModelTextures, NULL, "Number of textures on model", "number", return ade_set_args(L, "i", TM_NUM_TYPES * pm->n_textures); } -ADE_INDEXER(l_ModelTextures, "texture", "number Index/string TextureName", "texture", "Model textures, or invalid modeltextures handle if model handle is invalid") +ADE_INDEXER(l_ModelTextures, "number/string IndexOrTextureFilename", "Gets or sets a texture slot. A number is a 1-based index into the flat array (see the \"textures\" handle description for the layout); a string is matched against the filename of every slot on every material. Setting a slot changes the shared model, and so affects every object using it. The model takes its own reference to the texture, so the script does not need to keep the handle alive.", "texture", "Texture, or invalid texture handle if the model handle is invalid or the index/name does not match") { model_h *mth = NULL; texture_h* new_tex = nullptr; @@ -649,7 +649,7 @@ ADE_INDEXER(l_ModelTextures, "texture", "number Index/string TextureName", "text return ade_set_error(L, "o", l_Texture.Set(texture_h())); if (ADE_SETTING_VAR && new_tex != nullptr) { - tinfo->SetTexture(new_tex->handle); + tinfo->SetTexture(new_tex->handle, true); } return ade_set_args(L, "o", l_Texture.Set(texture_h(tinfo->GetTexture()))); diff --git a/code/scripting/api/objs/modelinstance.cpp b/code/scripting/api/objs/modelinstance.cpp index b93f70290a9..63e8e275c7f 100644 --- a/code/scripting/api/objs/modelinstance.cpp +++ b/code/scripting/api/objs/modelinstance.cpp @@ -11,9 +11,9 @@ namespace scripting { namespace api { //**********HANDLE: modelinstancetextures (compatible with preceding shiptextures) -ADE_OBJ(l_ModelInstanceTextures, modelinstance_h, "modelinstancetextures", "Model instance textures handle"); +ADE_OBJ(l_ModelInstanceTextures, modelinstance_h, "modelinstancetextures", "Flat array of textures for one model instance. It has the same layout as the model's \"textures\" handle: each material (texture_map) contributes " SCP_TOKEN_TO_STR(TM_NUM_TYPES) " consecutive entries, in this order: base, glow, specular, normal, height, misc, reflectance, ambient occlusion. So for material N (1-based), the base map is at index (N-1)*" SCP_TOKEN_TO_STR(TM_NUM_TYPES) "+1, the glow map at (N-1)*" SCP_TOKEN_TO_STR(TM_NUM_TYPES) "+2, and so on. Reads return the instance's replacement texture for that slot if one is set, otherwise the model's texture. Writes set a replacement texture on this instance only."); -ADE_FUNC(__len, l_ModelInstanceTextures, nullptr, "Number of textures on a model instance", "number", "Number of textures on the model instance, or 0 if handle is invalid") +ADE_FUNC(__len, l_ModelInstanceTextures, nullptr, "Number of texture slots on the model instance, i.e. the number of materials multiplied by " SCP_TOKEN_TO_STR(TM_NUM_TYPES), "number", "Number of texture slots, or 0 if handle is invalid") { modelinstance_h *mih; if(!ade_get_args(L, "o", l_ModelInstanceTextures.GetPtr(&mih))) @@ -30,7 +30,7 @@ ADE_FUNC(__len, l_ModelInstanceTextures, nullptr, "Number of textures on a model return ade_set_args(L, "i", pm->n_textures*TM_NUM_TYPES); } -ADE_INDEXER(l_ModelInstanceTextures, "number/string IndexOrTextureFilename", "Array of model instance textures", "texture", "Texture, or invalid texture handle on failure") +ADE_INDEXER(l_ModelInstanceTextures, "number/string IndexOrTextureFilename", "Gets or sets a texture slot. A number is a 1-based index into the flat array (see the \"modelinstancetextures\" handle description for the layout). A string is matched first against the filenames of this instance's replacement textures, then against the filename of every slot on every material of the model. Setting a slot sets a replacement texture on this instance only; set it to an invalid texture handle to clear the replacement. The instance takes its own reference to the texture, so the script does not need to keep the handle alive.", "texture", "Texture, or invalid texture handle if the handle is invalid or the index/name does not match") { modelinstance_h *mih; const char* s; @@ -86,9 +86,9 @@ ADE_INDEXER(l_ModelInstanceTextures, "number/string IndexOrTextureFilename", "Ar pmi->texture_replace = std::make_shared(); } - if (tdx != nullptr) { - (*pmi->texture_replace)[final_index] = tdx->isValid() ? tdx->handle : -1; - } + // an invalid texture handle clears the replacement + if (tdx != nullptr) + pmi->texture_replace->reference(final_index, tdx->handle); } if (pmi->texture_replace != nullptr && (*pmi->texture_replace)[final_index] >= 0) diff --git a/code/scripting/api/objs/shipclass.cpp b/code/scripting/api/objs/shipclass.cpp index bee25cf5458..0b264edc273 100644 --- a/code/scripting/api/objs/shipclass.cpp +++ b/code/scripting/api/objs/shipclass.cpp @@ -1320,10 +1320,6 @@ ADE_FUNC(renderSelectModel, render_info.set_team_color(tcolor, "none", 0, 0); } - if (sip->replacement_textures.size() > 0) { - render_info.set_replacement_textures(modelNum, sip->replacement_textures); - } - select_effect_params params; params.effect = effect; params.fs2_grid_color = sip->fs2_effect_grid_color; diff --git a/code/scripting/api/objs/texture.cpp b/code/scripting/api/objs/texture.cpp index 43c7228a41b..08aa039e1df 100644 --- a/code/scripting/api/objs/texture.cpp +++ b/code/scripting/api/objs/texture.cpp @@ -3,16 +3,14 @@ #include "texture.h" #include "bmpman/bmpman.h" -#define BMPMAN_INTERNAL -#include "bmpman/bm_internal.h" namespace scripting { namespace api { texture_h::texture_h() = default; -texture_h::texture_h(int bm, bool refcount, int parent_bm) : handle(bm), parent_handle(parent_bm) { +texture_h::texture_h(int bm, bool refcount) : handle(bm) { if (refcount && isValid()) - bm_get_entry(parent_bm != -1 ? parent_bm : bm)->load_count++; + bm_add_ref(bm); } texture_h::~texture_h() { @@ -31,7 +29,8 @@ texture_h::~texture_h() //Otherwise it is possible (and has been observed in practice) that the parent texture get's deleted before all dependent objects, //causing this release of the dependent object to clear unrelated textures that were assigned the previously freed spots. //So instead, both lock and later unlock the parent texture rather than this child texture. -Lafiel - bm_release(parent_handle != -1 ? parent_handle : handle); + //(bm_add_ref() and bm_release_ref() take care of that: both count on the first frame of an animation.) + bm_release_ref(handle); } bool texture_h::isValid() const { return bm_is_valid(handle) != 0; } @@ -41,7 +40,6 @@ texture_h::texture_h(texture_h&& other) noexcept { texture_h& texture_h::operator=(texture_h&& other) noexcept { if (this != &other) { std::swap(handle, other.handle); - std::swap(parent_handle, other.parent_handle); } return *this; } @@ -98,7 +96,7 @@ ADE_INDEXER(l_Texture, "number", //Get actual texture handle frame = first + frame; - return ade_set_args(L, "o", l_Texture.Set(texture_h(frame, true, first))); + return ade_set_args(L, "o", l_Texture.Set(texture_h(frame))); } ADE_FUNC(unload, l_Texture, NULL, "Unloads a texture from memory", NULL, NULL) @@ -111,7 +109,7 @@ ADE_FUNC(unload, l_Texture, NULL, "Unloads a texture from memory", NULL, NULL) if (!th->isValid()) return ADE_RETURN_NIL; - bm_release(th->handle); + bm_release_ref(th->handle); //WMC - invalidate this handle th->handle = -1; diff --git a/code/scripting/api/objs/texture.h b/code/scripting/api/objs/texture.h index 620fd8f5673..ec7a4343ad6 100644 --- a/code/scripting/api/objs/texture.h +++ b/code/scripting/api/objs/texture.h @@ -9,10 +9,9 @@ namespace api { struct texture_h { int handle = -1; - int parent_handle = -1; texture_h(); - explicit texture_h(int bm, bool refcount = true, int parent_handle = -1); + explicit texture_h(int bm, bool refcount = true); ~texture_h(); diff --git a/code/scripting/api/objs/texturemap.cpp b/code/scripting/api/objs/texturemap.cpp index 965222bb855..a7f439169d7 100644 --- a/code/scripting/api/objs/texturemap.cpp +++ b/code/scripting/api/objs/texturemap.cpp @@ -78,7 +78,7 @@ ADE_VIRTVAR(BaseMap, l_TextureMap, "texture", "Base texture", "texture", "Base t return ade_set_error(L, "o", l_Texture.Set(texture_h())); if (ADE_SETTING_VAR && new_tex != nullptr && new_tex->isValid()) { - tmap->textures[TM_BASE_TYPE].SetTexture(new_tex->handle); + tmap->textures[TM_BASE_TYPE].SetTexture(new_tex->handle, true); } return ade_set_args(L, "o", l_Texture.Set(texture_h(tmap->textures[TM_BASE_TYPE].GetTexture()))); @@ -96,7 +96,7 @@ ADE_VIRTVAR(GlowMap, l_TextureMap, "texture", "Glow texture", "texture", "Glow t return ade_set_error(L, "o", l_Texture.Set(texture_h())); if (ADE_SETTING_VAR && new_tex != nullptr && new_tex->isValid()) { - tmap->textures[TM_GLOW_TYPE].SetTexture(new_tex->handle); + tmap->textures[TM_GLOW_TYPE].SetTexture(new_tex->handle, true); } return ade_set_args(L, "o", l_Texture.Set(texture_h(tmap->textures[TM_GLOW_TYPE].GetTexture()))); @@ -114,7 +114,7 @@ ADE_VIRTVAR(SpecularMap, l_TextureMap, "texture", "Specular texture", "texture", return ade_set_error(L, "o", l_Texture.Set(texture_h())); if (ADE_SETTING_VAR && new_tex != nullptr && new_tex->isValid()) { - tmap->textures[TM_SPECULAR_TYPE].SetTexture(new_tex->handle); + tmap->textures[TM_SPECULAR_TYPE].SetTexture(new_tex->handle, true); } return ade_set_args(L, "o", l_Texture.Set(texture_h(tmap->textures[TM_SPECULAR_TYPE].GetTexture()))); diff --git a/code/ship/ship.cpp b/code/ship/ship.cpp index 1dc9250100c..bb9b3a0a1d6 100644 --- a/code/ship/ship.cpp +++ b/code/ship/ship.cpp @@ -7157,11 +7157,16 @@ void ship::apply_replacement_textures(const SCP_vector &replace int tnum = tmap->FindTexture(tr.old_texture); if (tnum > -1) - (*pmi->texture_replace)[j * TM_NUM_TYPES + tnum] = tr.new_texture_id; + pmi->texture_replace->reference(j * TM_NUM_TYPES + tnum, tr.new_texture_id); } } } +void ship::load_and_apply_replacement_textures(const SCP_vector &replacements) const +{ + model_instance_load_replacement_textures(model_get_instance(model_instance_num), replacements); +} + void ship_weapon::clear() { flags.reset(); @@ -8449,9 +8454,7 @@ void ship_close_cockpit_displays(ship* shipp) bm_release(Player_displays[i].foreground); } - if ( Player_displays[i].target >= 0 ) { - bm_release(Player_displays[i].target); - } + // the render target is owned by Player_cockpit_textures, not by the display } Player_displays.clear(); @@ -8487,8 +8490,14 @@ static void ship_add_cockpit_display(cockpit_display_info *display, int cockpit_ } } + // if the texture isn't on the model, there is nothing to draw to + if ( glow_target < 0 ) { + Warning(LOCATION, "Cockpit display '%s' draws to texture '%s', which is not on cockpit model '%s'. The display will not be created.", display->name, display->filename, pm->filename); + return; + } + // create a render target for this cockpit texture - auto& glow_texture = (*Player_cockpit_textures)[glow_target]; + int glow_texture = (*Player_cockpit_textures)[glow_target]; if ( glow_texture == -1) { bm_get_info(diffuse_handle, &w, &h); glow_texture = bm_make_render_target(w, h, BMP_FLAG_RENDER_TARGET_DYNAMIC | BMP_FLAG_RENDER_TARGET_DEPTH_ATTACHMENT); @@ -8497,6 +8506,8 @@ static void ship_add_cockpit_display(cockpit_display_info *display, int cockpit_ if ( glow_texture < 0 ) { return; } + + Player_cockpit_textures->adopt(glow_target, glow_texture); } new_display.background = -1; @@ -11595,14 +11606,12 @@ static void ship_model_change(int n, int ship_type) ship_info *sip; ship *sp; polymodel * pm; - polymodel_instance * pmi; object *objp; Assert( n >= 0 && n < MAX_SHIPS ); sp = &Ships[n]; sip = &(Ship_info[ship_type]); objp = &Objects[sp->objnum]; - pmi = model_get_instance(sp->model_instance_num); // get new model if (sip->model_num == -1) { @@ -11699,36 +11708,11 @@ static void ship_model_change(int n, int ship_type) sp->cockpit_model_instance = model_create_instance(model_objnum_special::OBJNUM_COCKPIT, sip->cockpit_model_num); else sp->cockpit_model_instance = -1; - - pmi = model_get_instance(sp->model_instance_num); // Goober5000 - deal with texture replacement by re-applying the same code we used during parsing // wookieejedi - replacement textures are loaded in mission parse, so need to load any new textures here // Lafiel - this now has to happen last, as the texture replacement stuff is stored in the pmi - if ( !sip->replacement_textures.empty() ) { - - // clear and reset replacement textures because the new positions may be different - pmi->texture_replace = std::make_shared(); - auto& texture_replace_deref = *pmi->texture_replace; - - // now fill them in according to texture name - for (const auto& tr : sip->replacement_textures) { - // look for textures - for (auto j = 0; j < pm->n_textures; j++) { - - texture_map* tmap = &pm->maps[j]; - int tnum = tmap->FindTexture(tr.old_texture); - - if (tnum > -1) { - // load new texture - int new_tex = bm_load_either(tr.new_texture); - if (new_tex > -1) { - texture_replace_deref[j * TM_NUM_TYPES + tnum] = new_tex; - } - } - } - } - } + sp->load_and_apply_replacement_textures(sip->replacement_textures); } /** diff --git a/code/ship/ship.h b/code/ship/ship.h index a0cb2b59894..468bae42f65 100644 --- a/code/ship/ship.h +++ b/code/ship/ship.h @@ -959,7 +959,11 @@ class ship const char* get_display_name() const; bool has_display_name() const; + // Applies replacement textures whose bitmaps have already been loaded (new_texture_id is set); the model instance takes its own references void apply_replacement_textures(const SCP_vector &replacements); + + // Applies replacement textures given by filename only (e.g. a ship class's table entry), loading the bitmaps for the model instance + void load_and_apply_replacement_textures(const SCP_vector &replacements) const; }; struct ai_target_priority { diff --git a/fred2/management.cpp b/fred2/management.cpp index 3d720b088fc..ea9fe56b7b5 100644 --- a/fred2/management.cpp +++ b/fred2/management.cpp @@ -650,17 +650,7 @@ int create_ship(matrix *orient, vec3d *pos, int ship_type) } Ai_info[shipp->ai_index].kamikaze_damage = (int) std::min(1000.0f, 200.0f + (temp_max_hull_strength / 4.0f)); - auto replacements = sip->replacement_textures; - for (auto& tr : replacements) { - if (!stricmp(tr.new_texture, "invisible")) { - // invisible is a special case - tr.new_texture_id = REPLACE_WITH_INVISIBLE; - } else { - // try to load texture or anim as normal - tr.new_texture_id = bm_load_either(tr.new_texture); - } - } - shipp->apply_replacement_textures(replacements); + shipp->load_and_apply_replacement_textures(sip->replacement_textures); return obj; } diff --git a/freespace2/freespace.cpp b/freespace2/freespace.cpp index 5ae02aa50d8..50af4191024 100644 --- a/freespace2/freespace.cpp +++ b/freespace2/freespace.cpp @@ -977,6 +977,7 @@ void game_level_close() ct_level_close(); beam_level_close(); mission_brief_common_reset(); // close out parsed briefing/mission stuff + mission_parse_level_close(); // let go of the replacement textures the parse objects were holding photo_mode_set_active(false); cam_close(); subtitles_close(); diff --git a/qtfred/src/mission/Editor.cpp b/qtfred/src/mission/Editor.cpp index c20ac7dc36a..d57d6406a62 100644 --- a/qtfred/src/mission/Editor.cpp +++ b/qtfred/src/mission/Editor.cpp @@ -755,17 +755,7 @@ int Editor::create_ship(matrix* orient, vec3d* pos, int ship_type) { } Ai_info[shipp->ai_index].kamikaze_damage = (int) std::min(1000.0f, 200.0f + (temp_max_hull_strength / 4.0f)); - auto replacements = sip->replacement_textures; - for (auto& tr : replacements) { - if (!stricmp(tr.new_texture, "invisible")) { - // invisible is a special case - tr.new_texture_id = REPLACE_WITH_INVISIBLE; - } else { - // try to load texture or anim as normal - tr.new_texture_id = bm_load_either(tr.new_texture); - } - } - shipp->apply_replacement_textures(replacements); + shipp->load_and_apply_replacement_textures(sip->replacement_textures); missionChanged(); return obj; diff --git a/qtfred/src/mission/dialogs/ShipEditor/ShipTextureReplacementDialogModel.cpp b/qtfred/src/mission/dialogs/ShipEditor/ShipTextureReplacementDialogModel.cpp index d590ea3f972..a1b1d5b9362 100644 --- a/qtfred/src/mission/dialogs/ShipEditor/ShipTextureReplacementDialogModel.cpp +++ b/qtfred/src/mission/dialogs/ShipEditor/ShipTextureReplacementDialogModel.cpp @@ -333,7 +333,7 @@ bool ShipTextureReplacementDialogModel::apply() if (!mainName.empty() && !lcase_equal(mainName, _defaultTextures[i])) { int id = load_tex(mainName); if (id != -1) - (*pmi->texture_replace)[groupIdx * TM_NUM_TYPES + TM_BASE_TYPE] = id; + pmi->texture_replace->adopt(groupIdx * TM_NUM_TYPES + TM_BASE_TYPE, id); } // Sub-type slots. @@ -356,7 +356,7 @@ bool ShipTextureReplacementDialogModel::apply() int id = load_tex(fullName); if (id != -1) - (*pmi->texture_replace)[groupIdx * TM_NUM_TYPES + tmType] = id; + pmi->texture_replace->adopt(groupIdx * TM_NUM_TYPES + tmType, id); } } } @@ -368,14 +368,19 @@ bool ShipTextureReplacementDialogModel::apply() continue; if (stricmp(tr.ship_name, shipp.ship_name) != 0) continue; - int id = (tr.new_texture_id != -1) ? tr.new_texture_id : load_tex(tr.new_texture); + // FRED entries never carry a loaded bitmap, so load it here + int id = load_tex(tr.new_texture); if (id == -1) continue; + // one bitmap may go into several slots, so each slot takes its own reference for (int j = 0; j < pm->n_textures; j++) { int tnum = pm->maps[j].FindTexture(tr.old_texture); if (tnum >= 0) - (*pmi->texture_replace)[j * TM_NUM_TYPES + tnum] = id; + pmi->texture_replace->reference(j * TM_NUM_TYPES + tnum, id); } + // and the reference from loading is no longer needed (the check skips REPLACE_WITH_INVISIBLE) + if (id >= 0) + bm_release_ref(id); } }; From b2112174a6870e2e771c9b1f77e7597e6d0cfec0 Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Fri, 4 Sep 2026 21:54:15 -0400 Subject: [PATCH 2/2] add the texture_map scripting handle and ship class replacement textures The "material" scripting class in texturemap.cpp had never been used since it was written in 2007. This reworks it into a usable texture_map handle and hooks it into the API. - texture_map_h now stores model number, model instance id (-1 for a model-level handle), and material index, resolving pointers on every access. The Lua type is "texture_map". - All eight slots are exposed as virtvars (BaseMap, GlowMap, SpecularMap, NormalMap, HeightMap, MiscMap, ReflectanceMap, AmbientOcclusionMap), plus a numeric indexer and __len, read-only Index, IsTransparent and IsAmbient, and resetToOriginal() (model level) / resetToModel() (instance level). Reads and writes follow the same rules as the flat "textures" and "modelinstancetextures" handles, including reference counting. - New "texturemaps" and "modelinstancetexturemaps" array handles, returned by a TextureMaps virtvar on model, model_instance, ship, and prop. The instance-level virtvars accept assignment exactly like Textures does. - The filename lookup of the two flat indexers is now shared with the new handles via model_find_texture_slot() and model_instance_find_texture_slot(). Both now return the first material whose slot matches; the model-level indexer previously returned the last. - New read-only "texture_replacement" / "texture_replacements" handles expose a ship class's table-defined replacement textures by filename, via a ReplacementTextures virtvar on shipclass. - Fix Prop.Textures assignment, which copied the prop's own replacement textures instead of the source prop's, making it a no-op. Co-Authored-By: Claude Fable 5.1 --- code/scripting/api/objs/model.cpp | 30 +- code/scripting/api/objs/modelinstance.cpp | 49 +- code/scripting/api/objs/prop.cpp | 28 +- code/scripting/api/objs/ship.cpp | 23 +- code/scripting/api/objs/shipclass.cpp | 16 + .../api/objs/texture_replacement.cpp | 143 ++++++ code/scripting/api/objs/texture_replacement.h | 47 ++ code/scripting/api/objs/texturemap.cpp | 470 +++++++++++++++--- code/scripting/api/objs/texturemap.h | 51 +- code/scripting/lua.cpp | 1 + code/source_groups.cmake | 2 + 11 files changed, 725 insertions(+), 135 deletions(-) create mode 100644 code/scripting/api/objs/texture_replacement.cpp create mode 100644 code/scripting/api/objs/texture_replacement.h diff --git a/code/scripting/api/objs/model.cpp b/code/scripting/api/objs/model.cpp index 7e2e7914b78..2ccd1aa81c2 100644 --- a/code/scripting/api/objs/model.cpp +++ b/code/scripting/api/objs/model.cpp @@ -5,6 +5,7 @@ #include "vecmath.h" #include "eye.h" #include "texture.h" +#include "texturemap.h" extern void model_calc_bound_box(vec3d *box, const vec3d *big_mn, const vec3d *big_mx); @@ -123,6 +124,22 @@ ADE_VIRTVAR(Textures, l_Model, nullptr, "Model textures", "textures", "Model tex return ade_set_args(L, "o", l_ModelTextures.Set(model_h(pm))); } +ADE_VIRTVAR(TextureMaps, l_Model, nullptr, "Model materials", "texturemaps", "Array of the model's materials, or an invalid texturemaps handle if the model handle is invalid") +{ + model_h *mdl = nullptr; + if (!ade_get_args(L, "o", l_Model.GetPtr(&mdl))) + return ade_set_error(L, "o", l_ModelTextureMaps.Set(model_h())); + + polymodel *pm = mdl->Get(); + if (!pm) + return ade_set_error(L, "o", l_ModelTextureMaps.Set(model_h())); + + if (ADE_SETTING_VAR) + LuaError(L, "Assigning texture maps is not supported"); + + return ade_set_args(L, "o", l_ModelTextureMaps.Set(model_h(pm))); +} + ADE_VIRTVAR(Thrusters, l_Model, nullptr, "Model thrusters", "thrusters", "Model thrusters, or an invalid thrusters handle if the model handle is invalid") { model_h *mdl = nullptr; @@ -590,7 +607,7 @@ ADE_INDEXER(l_ModelSubmodels, "submodel", "number|string IndexOrName", "submodel //**********HANDLE: modeltextures -ADE_OBJ(l_ModelTextures, model_h, "textures", "Flat array of model textures. Each material (texture_map) on the model contributes " SCP_TOKEN_TO_STR(TM_NUM_TYPES) " consecutive entries, in this order: base, glow, specular, normal, height, misc, reflectance, ambient occlusion. So for material N (1-based), the base map is at index (N-1)*" SCP_TOKEN_TO_STR(TM_NUM_TYPES) "+1, the glow map at (N-1)*" SCP_TOKEN_TO_STR(TM_NUM_TYPES) "+2, and so on. Slots that the material does not use hold invalid texture handles."); +ADE_OBJ(l_ModelTextures, model_h, "textures", "Flat array of model textures. Each material (texture_map) on the model contributes " SCP_TOKEN_TO_STR(TM_NUM_TYPES) " consecutive entries, in this order: base, glow, specular, normal, height, misc, reflectance, ambient occlusion. So for material N (1-based), the base map is at index (N-1)*" SCP_TOKEN_TO_STR(TM_NUM_TYPES) "+1, the glow map at (N-1)*" SCP_TOKEN_TO_STR(TM_NUM_TYPES) "+2, and so on. Slots that the material does not use hold invalid texture handles. The TextureMaps array exposes the same slots grouped per material, as texture_map handles."); ADE_FUNC(__len, l_ModelTextures, nullptr, "Number of texture slots on the model, i.e. the number of materials multiplied by " SCP_TOKEN_TO_STR(TM_NUM_TYPES), "number", "Number of texture slots, or 0 if handle is invalid") { @@ -635,14 +652,9 @@ ADE_INDEXER(l_ModelTextures, "number/string IndexOrTextureFilename", "Gets or se if(tinfo == NULL) { - for (int i = 0; i < pm->n_textures; i++) - { - tmap = &pm->maps[i]; - - int tnum = tmap->FindTexture(s); - if(tnum > -1) - tinfo = &tmap->textures[tnum]; - } + int slot = model_find_texture_slot(pm, s); + if (slot >= 0) + tinfo = &pm->maps[slot / TM_NUM_TYPES].textures[slot % TM_NUM_TYPES]; } if(tinfo == NULL) diff --git a/code/scripting/api/objs/modelinstance.cpp b/code/scripting/api/objs/modelinstance.cpp index 63e8e275c7f..10757788fc2 100644 --- a/code/scripting/api/objs/modelinstance.cpp +++ b/code/scripting/api/objs/modelinstance.cpp @@ -6,12 +6,13 @@ #include "object.h" #include "vecmath.h" #include "texture.h" +#include "texturemap.h" namespace scripting { namespace api { //**********HANDLE: modelinstancetextures (compatible with preceding shiptextures) -ADE_OBJ(l_ModelInstanceTextures, modelinstance_h, "modelinstancetextures", "Flat array of textures for one model instance. It has the same layout as the model's \"textures\" handle: each material (texture_map) contributes " SCP_TOKEN_TO_STR(TM_NUM_TYPES) " consecutive entries, in this order: base, glow, specular, normal, height, misc, reflectance, ambient occlusion. So for material N (1-based), the base map is at index (N-1)*" SCP_TOKEN_TO_STR(TM_NUM_TYPES) "+1, the glow map at (N-1)*" SCP_TOKEN_TO_STR(TM_NUM_TYPES) "+2, and so on. Reads return the instance's replacement texture for that slot if one is set, otherwise the model's texture. Writes set a replacement texture on this instance only."); +ADE_OBJ(l_ModelInstanceTextures, modelinstance_h, "modelinstancetextures", "Flat array of textures for one model instance. It has the same layout as the model's \"textures\" handle: each material (texture_map) contributes " SCP_TOKEN_TO_STR(TM_NUM_TYPES) " consecutive entries, in this order: base, glow, specular, normal, height, misc, reflectance, ambient occlusion. So for material N (1-based), the base map is at index (N-1)*" SCP_TOKEN_TO_STR(TM_NUM_TYPES) "+1, the glow map at (N-1)*" SCP_TOKEN_TO_STR(TM_NUM_TYPES) "+2, and so on. Reads return the instance's replacement texture for that slot if one is set, otherwise the model's texture. Writes set a replacement texture on this instance only. The TextureMaps array exposes the same slots grouped per material, as texture_map handles."); ADE_FUNC(__len, l_ModelInstanceTextures, nullptr, "Number of texture slots on the model instance, i.e. the number of materials multiplied by " SCP_TOKEN_TO_STR(TM_NUM_TYPES), "number", "Number of texture slots, or 0 if handle is invalid") { @@ -43,35 +44,7 @@ ADE_INDEXER(l_ModelInstanceTextures, "number/string IndexOrTextureFilename", "Ge polymodel_instance *pmi = mih->Get(); polymodel *pm = model_get(pmi->model_num); - int final_index = -1; - int i; - - char fname[MAX_FILENAME_LEN]; - if (pmi->texture_replace != nullptr) - { - for(i = 0; i < MAX_REPLACEMENT_TEXTURES; i++) - { - bm_get_filename((*pmi->texture_replace)[i], fname); - - if(!strextcmp(fname, s)) { - final_index = i; - break; - } - } - } - - if(final_index < 0) - { - for (i = 0; i < pm->n_textures; i++) - { - int tm_num = pm->maps[i].FindTexture(s); - if(tm_num > -1) - { - final_index = i*TM_NUM_TYPES+tm_num; - break; - } - } - } + int final_index = model_instance_find_texture_slot(pmi, pm, s); if (final_index < 0) { @@ -253,6 +226,22 @@ ADE_VIRTVAR(Textures, l_ModelInstance, "modelinstancetextures", "Gets model inst return ade_set_args(L, "o", l_ModelInstanceTextures.Set(modelinstance_h(dh->Get()))); } +ADE_VIRTVAR(TextureMaps, l_ModelInstance, "modelinstancetexturemaps", "Gets the model instance's materials. Assigning another instance's TextureMaps makes this instance share that instance's replacement textures, exactly as assigning Textures does.", "modelinstancetexturemaps", "Array of the model instance's materials, or invalid modelinstancetexturemaps handle if modelinstance handle is invalid") +{ + modelinstance_h *sh = nullptr; + modelinstance_h *dh; + if(!ade_get_args(L, "o|o", l_ModelInstance.GetPtr(&dh), l_ModelInstance.GetPtr(&sh))) + return ade_set_error(L, "o", l_ModelInstanceTextureMaps.Set(modelinstance_h())); + + if(!dh->isValid()) + return ade_set_error(L, "o", l_ModelInstanceTextureMaps.Set(modelinstance_h())); + + if(ADE_SETTING_VAR && sh && sh->isValid()) + dh->Get()->texture_replace = sh->Get()->texture_replace; + + return ade_set_args(L, "o", l_ModelInstanceTextureMaps.Set(modelinstance_h(dh->Get()))); +} + ADE_VIRTVAR(SubmodelInstances, l_ModelInstance, nullptr, "Submodel instances", "submodel_instances", "Model submodel instances, or an invalid modelsubmodelinstances handle if the model instance handle is invalid") { modelinstance_h *mih = nullptr; diff --git a/code/scripting/api/objs/prop.cpp b/code/scripting/api/objs/prop.cpp index ae93e10b925..b4764b0f784 100644 --- a/code/scripting/api/objs/prop.cpp +++ b/code/scripting/api/objs/prop.cpp @@ -7,6 +7,7 @@ #include "object.h" #include "prop.h" #include "propclass.h" +#include "texturemap.h" #include "prop/prop.h" @@ -88,10 +89,35 @@ ADE_VIRTVAR(Textures, polymodel_instance* dest = model_get_instance(propp->model_instance_num); if (ADE_SETTING_VAR && sh && sh->isValid()) { - dest->texture_replace = model_get_instance(propp->model_instance_num)->texture_replace; + dest->texture_replace = model_get_instance(prop_id_lookup(sh->objp()->instance)->model_instance_num)->texture_replace; } return ade_set_args(L, "o", l_ModelInstanceTextures.Set(modelinstance_h(dest))); } +ADE_VIRTVAR(TextureMaps, + l_Prop, + "modelinstancetexturemaps", + "Gets the prop's materials. Assigning another prop's TextureMaps makes this prop share that prop's replacement textures, exactly as assigning Textures does.", + "modelinstancetexturemaps", + "Array of the prop's materials, or invalid modelinstancetexturemaps handle if prop handle is invalid") +{ + object_h* sh = nullptr; + object_h* dh; + if (!ade_get_args(L, "o|o", l_Prop.GetPtr(&dh), l_Prop.GetPtr(&sh))) + return ade_set_error(L, "o", l_ModelInstanceTextureMaps.Set(modelinstance_h())); + + if (!dh->isValid()) + return ade_set_error(L, "o", l_ModelInstanceTextureMaps.Set(modelinstance_h())); + + prop* propp = prop_id_lookup(dh->objp()->instance); + + polymodel_instance* dest = model_get_instance(propp->model_instance_num); + + if (ADE_SETTING_VAR && sh && sh->isValid()) + dest->texture_replace = model_get_instance(prop_id_lookup(sh->objp()->instance)->model_instance_num)->texture_replace; + + return ade_set_args(L, "o", l_ModelInstanceTextureMaps.Set(modelinstance_h(dest))); +} + } // namespace scripting::api diff --git a/code/scripting/api/objs/ship.cpp b/code/scripting/api/objs/ship.cpp index 36c3dccaeb8..7202c8dd2d7 100644 --- a/code/scripting/api/objs/ship.cpp +++ b/code/scripting/api/objs/ship.cpp @@ -18,6 +18,7 @@ #include "team.h" #include "team_colors.h" #include "texture.h" +#include "texturemap.h" #include "vecmath.h" #include "weaponclass.h" #include "wing.h" @@ -959,14 +960,30 @@ ADE_VIRTVAR(Textures, l_Ship, "modelinstancetextures", "Gets ship textures", "mo polymodel_instance *dest = model_get_instance(Ships[dh->objp()->instance].model_instance_num); - if(ADE_SETTING_VAR && sh && sh->isValid()) { + if(ADE_SETTING_VAR && sh && sh->isValid()) dest->texture_replace = model_get_instance(Ships[sh->objp()->instance].model_instance_num)->texture_replace; - - } return ade_set_args(L, "o", l_ModelInstanceTextures.Set(modelinstance_h(dest))); } +ADE_VIRTVAR(TextureMaps, l_Ship, "modelinstancetexturemaps", "Gets the ship's materials. Assigning another ship's TextureMaps makes this ship share that ship's replacement textures, exactly as assigning Textures does.", "modelinstancetexturemaps", "Array of the ship's materials, or invalid modelinstancetexturemaps handle if ship handle is invalid") +{ + object_h *sh = nullptr; + object_h *dh; + if(!ade_get_args(L, "o|o", l_Ship.GetPtr(&dh), l_Ship.GetPtr(&sh))) + return ade_set_error(L, "o", l_ModelInstanceTextureMaps.Set(modelinstance_h())); + + if(!dh->isValid()) + return ade_set_error(L, "o", l_ModelInstanceTextureMaps.Set(modelinstance_h())); + + polymodel_instance *dest = model_get_instance(Ships[dh->objp()->instance].model_instance_num); + + if(ADE_SETTING_VAR && sh && sh->isValid()) + dest->texture_replace = model_get_instance(Ships[sh->objp()->instance].model_instance_num)->texture_replace; + + return ade_set_args(L, "o", l_ModelInstanceTextureMaps.Set(modelinstance_h(dest))); +} + ADE_VIRTVAR(FlagAffectedByGravity, l_Ship, "boolean", "Checks for the \"affected-by-gravity\" flag", "boolean", "True if flag is set, false if flag is not set and nil on error") { object_h *objh=NULL; diff --git a/code/scripting/api/objs/shipclass.cpp b/code/scripting/api/objs/shipclass.cpp index 0b264edc273..754ae65a60a 100644 --- a/code/scripting/api/objs/shipclass.cpp +++ b/code/scripting/api/objs/shipclass.cpp @@ -9,6 +9,7 @@ #include "species.h" #include "shiptype.h" #include "team_colors.h" +#include "texture_replacement.h" #include "vecmath.h" #include "ship/ship.h" #include "playerman/player.h" @@ -656,6 +657,21 @@ ADE_VIRTVAR(CockpitDisplays, l_Shipclass, "cockpitdisplays", "Gets the cockpit d return ade_set_args(L, "o", l_CockpitDisplayInfos.Set(cockpit_displays_info_h(ship_info_idx))); } +ADE_VIRTVAR(ReplacementTextures, l_Shipclass, nullptr, "Gets the replacement textures defined in the table entry of this ship class", "texture_replacements", "Array handle, or invalid texture_replacements handle if the shipclass handle is invalid") +{ + int ship_info_idx = -1; + if (!ade_get_args(L, "o", l_Shipclass.Get(&ship_info_idx))) + return ade_set_error(L, "o", l_ShipclassTextureReplacements.Set(shipclass_texture_replacements_h())); + + if (ship_info_idx < 0 || ship_info_idx >= ship_info_size()) + return ade_set_error(L, "o", l_ShipclassTextureReplacements.Set(shipclass_texture_replacements_h())); + + if (ADE_SETTING_VAR) + LuaError(L, "This property is read only."); + + return ade_set_args(L, "o", l_ShipclassTextureReplacements.Set(shipclass_texture_replacements_h(ship_info_idx))); +} + ADE_VIRTVAR(HitpointsMax, l_Shipclass, "number", "Ship class hitpoints", "number", "Hitpoints, or 0 if handle is invalid") { int idx; diff --git a/code/scripting/api/objs/texture_replacement.cpp b/code/scripting/api/objs/texture_replacement.cpp new file mode 100644 index 00000000000..e736017652e --- /dev/null +++ b/code/scripting/api/objs/texture_replacement.cpp @@ -0,0 +1,143 @@ +// +// + +#include "texture_replacement.h" + +namespace scripting::api +{ + +texture_replacement_h::texture_replacement_h() + : m_ship_info_idx(-1), m_index(INVALID_ID) +{} +texture_replacement_h::texture_replacement_h(int ship_info_idx, size_t index) + : m_ship_info_idx(ship_info_idx), m_index(index) +{} +const texture_replace *texture_replacement_h::Get() const +{ + return isValid() ? &Ship_info[m_ship_info_idx].replacement_textures[m_index] : nullptr; +} +bool texture_replacement_h::isValid() const +{ + return Ship_info.in_bounds(m_ship_info_idx) && m_index < Ship_info[m_ship_info_idx].replacement_textures.size(); +} + + +//**********HANDLE: texture_replacement +ADE_OBJ(l_TextureReplacement, texture_replacement_h, "texture_replacement", "One replacement texture of a ship class, as defined in its table entry: the filename of a model texture and the filename of the texture that replaces it on every ship of the class. Only the filenames are available here; the replacement bitmaps are loaded for each ship when it is created, and can be inspected through the ship's Textures or TextureMaps."); + +ADE_VIRTVAR(OldFilename, l_TextureReplacement, nullptr, "Filename of the model texture that is replaced", "string", "Filename, or empty string if handle is invalid") +{ + texture_replacement_h *trh = nullptr; + if (!ade_get_args(L, "o", l_TextureReplacement.GetPtr(&trh))) + return ade_set_error(L, "s", ""); + + auto tr = trh->Get(); + if (tr == nullptr) + return ade_set_error(L, "s", ""); + + if (ADE_SETTING_VAR) + LuaError(L, "This property is read only."); + + return ade_set_args(L, "s", tr->old_texture); +} + +ADE_VIRTVAR(NewFilename, l_TextureReplacement, nullptr, "Filename of the texture that replaces it", "string", "Filename, or empty string if handle is invalid") +{ + texture_replacement_h *trh = nullptr; + if (!ade_get_args(L, "o", l_TextureReplacement.GetPtr(&trh))) + return ade_set_error(L, "s", ""); + + auto tr = trh->Get(); + if (tr == nullptr) + return ade_set_error(L, "s", ""); + + if (ADE_SETTING_VAR) + LuaError(L, "This property is read only."); + + return ade_set_args(L, "s", tr->new_texture); +} + + +shipclass_texture_replacements_h::shipclass_texture_replacements_h() + : m_ship_info_idx(-1) +{} +shipclass_texture_replacements_h::shipclass_texture_replacements_h(int ship_info_idx) + : m_ship_info_idx(ship_info_idx) +{} +const ship_info *shipclass_texture_replacements_h::GetShipInfoPtr() const +{ + return isValid() ? &Ship_info[m_ship_info_idx] : nullptr; +} +int shipclass_texture_replacements_h::GetShipInfoIndex() const +{ + return isValid() ? m_ship_info_idx : -1; +} +bool shipclass_texture_replacements_h::isValid() const +{ + return Ship_info.in_bounds(m_ship_info_idx); +} + + +//**********HANDLE: texture_replacements +ADE_OBJ(l_ShipclassTextureReplacements, shipclass_texture_replacements_h, "texture_replacements", "Array of a ship class's replacement textures"); + +ADE_FUNC(__len, l_ShipclassTextureReplacements, nullptr, "Number of replacement textures defined for the ship class", "number", "Number of replacement textures, or 0 if handle is invalid") +{ + shipclass_texture_replacements_h *trh = nullptr; + if (!ade_get_args(L, "o", l_ShipclassTextureReplacements.GetPtr(&trh))) + return ade_set_error(L, "i", 0); + + auto sip = trh->GetShipInfoPtr(); + if (sip == nullptr) + return ade_set_error(L, "i", 0); + + return ade_set_args(L, "i", static_cast(sip->replacement_textures.size())); +} + +ADE_INDEXER(l_ShipclassTextureReplacements, "number/string IndexOrOldFilename", "Gets a replacement texture by 1-based index, or by the filename of the model texture it replaces", "texture_replacement", "Replacement texture handle, or invalid texture_replacement handle if the handle is invalid or nothing matches") +{ + shipclass_texture_replacements_h *trh = nullptr; + size_t index = INVALID_ID; + + if (lua_isnumber(L, 2)) + { + int lua_index = -1; + if (!ade_get_args(L, "oi", l_ShipclassTextureReplacements.GetPtr(&trh), &lua_index)) + return ade_set_error(L, "o", l_TextureReplacement.Set(texture_replacement_h())); + + if (lua_index < 1) + return ade_set_error(L, "o", l_TextureReplacement.Set(texture_replacement_h())); + + index = static_cast(lua_index - 1); // Lua -> FS2 + } + else + { + const char *name = nullptr; + if (!ade_get_args(L, "os", l_ShipclassTextureReplacements.GetPtr(&trh), &name)) + return ade_set_error(L, "o", l_TextureReplacement.Set(texture_replacement_h())); + + auto sip = trh->GetShipInfoPtr(); + if (sip == nullptr || name == nullptr) + return ade_set_error(L, "o", l_TextureReplacement.Set(texture_replacement_h())); + + for (size_t i = 0; i < sip->replacement_textures.size(); i++) + { + if (!strextcmp(sip->replacement_textures[i].old_texture, name)) + { + index = i; + break; + } + } + } + + auto sip = trh->GetShipInfoPtr(); + if (sip == nullptr || index >= sip->replacement_textures.size()) + return ade_set_error(L, "o", l_TextureReplacement.Set(texture_replacement_h())); + + if (ADE_SETTING_VAR) + LuaError(L, "Replacement textures are read only."); + + return ade_set_args(L, "o", l_TextureReplacement.Set(texture_replacement_h(trh->GetShipInfoIndex(), index))); +} + +} diff --git a/code/scripting/api/objs/texture_replacement.h b/code/scripting/api/objs/texture_replacement.h new file mode 100644 index 00000000000..8286eeafb3c --- /dev/null +++ b/code/scripting/api/objs/texture_replacement.h @@ -0,0 +1,47 @@ +#pragma once + +#include "scripting/ade_api.h" +#include "mission/missionparse.h" +#include "ship/ship.h" + +namespace scripting::api +{ + +// One replacement texture of a ship class, as defined in the class's table entry +class texture_replacement_h +{ + private: + int m_ship_info_idx; + size_t m_index; + + public: + texture_replacement_h(); + explicit texture_replacement_h(int ship_info_idx, size_t index); + + const texture_replace *Get() const; + + bool isValid() const; +}; + +DECLARE_ADE_OBJ(l_TextureReplacement, texture_replacement_h); + + +// The array of a ship class's replacement textures +class shipclass_texture_replacements_h +{ + private: + int m_ship_info_idx; + + public: + shipclass_texture_replacements_h(); + explicit shipclass_texture_replacements_h(int ship_info_idx); + + const ship_info *GetShipInfoPtr() const; + int GetShipInfoIndex() const; + + bool isValid() const; +}; + +DECLARE_ADE_OBJ(l_ShipclassTextureReplacements, shipclass_texture_replacements_h); + +} diff --git a/code/scripting/api/objs/texturemap.cpp b/code/scripting/api/objs/texturemap.cpp index a7f439169d7..5d5ade62f89 100644 --- a/code/scripting/api/objs/texturemap.cpp +++ b/code/scripting/api/objs/texturemap.cpp @@ -4,121 +4,445 @@ #include "texturemap.h" #include "texture.h" -namespace scripting { -namespace api { +#include "bmpman/bmpman.h" -texture_map_h::texture_map_h() { - type = THT_INDEPENDENT; - tmap = NULL; +namespace scripting::api +{ + +int model_find_texture_slot(polymodel *pm, const char *name) +{ + if (pm == nullptr || name == nullptr) + return -1; + + for (int i = 0; i < pm->n_textures; i++) + { + int tnum = pm->maps[i].FindTexture(name); + if (tnum >= 0) + return i * TM_NUM_TYPES + tnum; + } + + return -1; +} + +int model_instance_find_texture_slot(polymodel_instance *pmi, polymodel *pm, const char *name) +{ + if (pmi == nullptr || name == nullptr) + return -1; + + if (pmi->texture_replace != nullptr) + { + char fname[MAX_FILENAME_LEN]; + + for (int i = 0; i < MAX_REPLACEMENT_TEXTURES; i++) + { + int handle = (*pmi->texture_replace)[i]; + if (handle < 0) + continue; + + bm_get_filename(handle, fname); + if (!strextcmp(fname, name)) + return i; + } + } + + return model_find_texture_slot(pm, name); +} + + +texture_map_h::texture_map_h() + : model_num(-1), pmi_id(-1), map_index(-1) +{} +texture_map_h::texture_map_h(polymodel *pm, int n_map_index) + : model_num(pm ? pm->id : -1), pmi_id(-1), map_index(n_map_index) +{} +texture_map_h::texture_map_h(polymodel_instance *pmi, int n_map_index) + : model_num(pmi ? pmi->model_num : -1), pmi_id(pmi ? pmi->id : -1), map_index(n_map_index) +{} + +polymodel *texture_map_h::GetModel() const +{ + return isValid() ? model_get(model_num) : nullptr; +} +polymodel_instance *texture_map_h::GetModelInstance() const +{ + return (isValid() && pmi_id >= 0) ? model_get_instance(pmi_id) : nullptr; +} +texture_map *texture_map_h::Get() const +{ + return isValid() ? &model_get(model_num)->maps[map_index] : nullptr; } -texture_map_h::texture_map_h(object* objp, texture_map* n_tmap) { - type = THT_OBJECT; - obj = object_h(objp); - tmap = n_tmap; + +int texture_map_h::GetModelID() const +{ + return model_num; } -texture_map_h::texture_map_h(int modelnum, texture_map* n_tmap) { - type = THT_MODEL; - mdl = model_h(modelnum); - tmap = n_tmap; +int texture_map_h::GetModelInstanceID() const +{ + return pmi_id; +} +int texture_map_h::GetIndex() const +{ + return map_index; } -texture_map_h::texture_map_h(polymodel* n_model, texture_map* n_tmap) { - type = THT_MODEL; - mdl = model_h(n_model); - tmap = n_tmap; + +bool texture_map_h::isInstance() const +{ + return pmi_id >= 0; } -texture_map* texture_map_h::Get() { - if(!this->isValid()) - return NULL; - return tmap; +int texture_map_h::GetSlotTexture(int slot) const +{ + if (!isValid() || slot < 0 || slot >= TM_NUM_TYPES) + return -1; + + if (pmi_id >= 0) + { + auto pmi = model_get_instance(pmi_id); + if (pmi->texture_replace != nullptr) + { + int replacement = (*pmi->texture_replace)[map_index * TM_NUM_TYPES + slot]; + if (replacement >= 0) + return replacement; + } + } + + return model_get(model_num)->maps[map_index].textures[slot].GetTexture(); } -int texture_map_h::GetSize() { - if(!this->isValid()) - return 0; - switch(type) +void texture_map_h::SetSlotTexture(int slot, int bm_handle) const +{ + if (!isValid() || slot < 0 || slot >= TM_NUM_TYPES) + return; + + if (pmi_id >= 0) + { + auto pmi = model_get_instance(pmi_id); + if (pmi->texture_replace == nullptr) + pmi->texture_replace = std::make_shared(); + + // an invalid handle clears the replacement + pmi->texture_replace->reference(map_index * TM_NUM_TYPES + slot, bm_handle); + } + else { - case THT_MODEL: - return mdl.Get()->n_textures; - case THT_OBJECT: - return 0; //Can't do this right now. - default: - return 0; + // an invalid handle blanks the slot + model_get(model_num)->maps[map_index].textures[slot].SetTexture(bm_is_valid(bm_handle) ? bm_handle : -1, true); } } -bool texture_map_h::isValid() const { - if(tmap == NULL) + +bool texture_map_h::isValid() const +{ + if (model_num < 0 || map_index < 0) return false; - switch(type) + if (pmi_id >= 0) { - case THT_INDEPENDENT: - return true; - case THT_OBJECT: - return obj.isValid(); - case THT_MODEL: - return mdl.isValid(); - default: - Error(LOCATION, "Bad type in texture_map_h; debug this."); + if (pmi_id >= num_model_instances()) + return false; + + auto pmi = model_get_instance(pmi_id); + if (pmi == nullptr || pmi->model_num != model_num) return false; } + + auto pm = model_get(model_num); + return pm != nullptr && map_index < pm->n_textures; } -ADE_OBJ(l_TextureMap, texture_map_h, "material", "Texture map, including diffuse, glow, and specular textures"); -ADE_VIRTVAR(BaseMap, l_TextureMap, "texture", "Base texture", "texture", "Base texture, or invalid texture handle if material handle is invalid") +//**********HANDLE: texture_map +ADE_OBJ(l_TextureMap, texture_map_h, "texture_map", "One material of a model: the set of " SCP_TOKEN_TO_STR(TM_NUM_TYPES) " texture slots (base, glow, specular, normal, height, misc, reflectance, ambient occlusion) that the model draws a group of polygons with. Handles come from the TextureMaps array of a model or of a model instance (ship, prop, etc.). A model-level handle reads and writes the shared model, and so affects every object using it. An instance-level handle reads the instance's replacement texture for a slot if one is set (otherwise the model's texture) and writes replacement textures on that instance only. The flat \"textures\" and \"modelinstancetextures\" handles expose the same slots as one array with " SCP_TOKEN_TO_STR(TM_NUM_TYPES) " entries per material."); + +// Setter documentation shared by every slot accessor +#define TEXTURE_MAP_SET_DOC "Setting a model-level handle changes the shared model; setting an instance-level handle sets a replacement texture on that instance only. Assign an invalid texture handle (not nil, which does nothing) to blank the slot at model level or to clear the replacement at instance level. The model or instance takes its own reference to the texture, so the script does not need to keep the handle alive." + +// Gets or sets one slot of a texture map; the caller has already parsed the arguments +static int texture_map_slot(lua_State *L, texture_map_h *tmh, int slot, texture_h *new_tex) { - texture_map_h *tmh = NULL; - texture_h* new_tex = nullptr; - if(!ade_get_args(L, "o|o", l_TextureMap.GetPtr(&tmh), l_Texture.GetPtr(&new_tex))) + if (tmh == nullptr || !tmh->isValid()) return ade_set_error(L, "o", l_Texture.Set(texture_h())); - texture_map *tmap = tmh->Get(); - if(tmap == NULL) + if (ADE_SETTING_VAR && new_tex != nullptr) + tmh->SetSlotTexture(slot, new_tex->handle); + + return ade_set_args(L, "o", l_Texture.Set(texture_h(tmh->GetSlotTexture(slot)))); +} + +// Parses the arguments of a slot virtvar and gets or sets that slot +static int texture_map_slot_virtvar(lua_State *L, int slot) +{ + texture_map_h *tmh = nullptr; + texture_h *new_tex = nullptr; + if (!ade_get_args(L, "o|o", l_TextureMap.GetPtr(&tmh), l_Texture.GetPtr(&new_tex))) return ade_set_error(L, "o", l_Texture.Set(texture_h())); - if (ADE_SETTING_VAR && new_tex != nullptr && new_tex->isValid()) { - tmap->textures[TM_BASE_TYPE].SetTexture(new_tex->handle, true); - } + return texture_map_slot(L, tmh, slot, new_tex); +} + +ADE_VIRTVAR(BaseMap, l_TextureMap, "texture", "Base (diffuse) texture. " TEXTURE_MAP_SET_DOC, "texture", "Base texture, or invalid texture handle if the handle is invalid") +{ + return texture_map_slot_virtvar(L, TM_BASE_TYPE); +} + +ADE_VIRTVAR(GlowMap, l_TextureMap, "texture", "Glow texture (-glow). " TEXTURE_MAP_SET_DOC, "texture", "Glow texture, or invalid texture handle if the handle is invalid") +{ + return texture_map_slot_virtvar(L, TM_GLOW_TYPE); +} + +ADE_VIRTVAR(SpecularMap, l_TextureMap, "texture", "Specular texture (-shine). " TEXTURE_MAP_SET_DOC, "texture", "Specular texture, or invalid texture handle if the handle is invalid") +{ + return texture_map_slot_virtvar(L, TM_SPECULAR_TYPE); +} + +ADE_VIRTVAR(NormalMap, l_TextureMap, "texture", "Normal texture (-normal). " TEXTURE_MAP_SET_DOC, "texture", "Normal texture, or invalid texture handle if the handle is invalid") +{ + return texture_map_slot_virtvar(L, TM_NORMAL_TYPE); +} + +ADE_VIRTVAR(HeightMap, l_TextureMap, "texture", "Height texture (-height), used for parallax mapping. " TEXTURE_MAP_SET_DOC, "texture", "Height texture, or invalid texture handle if the handle is invalid") +{ + return texture_map_slot_virtvar(L, TM_HEIGHT_TYPE); +} + +ADE_VIRTVAR(MiscMap, l_TextureMap, "texture", "Misc (utility) texture (-misc). " TEXTURE_MAP_SET_DOC, "texture", "Misc texture, or invalid texture handle if the handle is invalid") +{ + return texture_map_slot_virtvar(L, TM_MISC_TYPE); +} + +ADE_VIRTVAR(ReflectanceMap, l_TextureMap, "texture", "Reflectance texture (-reflect), holding specular and gloss. " TEXTURE_MAP_SET_DOC, "texture", "Reflectance texture, or invalid texture handle if the handle is invalid") +{ + return texture_map_slot_virtvar(L, TM_SPEC_GLOSS_TYPE); +} - return ade_set_args(L, "o", l_Texture.Set(texture_h(tmap->textures[TM_BASE_TYPE].GetTexture()))); +ADE_VIRTVAR(AmbientOcclusionMap, l_TextureMap, "texture", "Ambient occlusion texture (-ao). " TEXTURE_MAP_SET_DOC, "texture", "Ambient occlusion texture, or invalid texture handle if the handle is invalid") +{ + return texture_map_slot_virtvar(L, TM_AMBIENT_TYPE); } -ADE_VIRTVAR(GlowMap, l_TextureMap, "texture", "Glow texture", "texture", "Glow texture, or invalid texture handle if material handle is invalid") +ADE_INDEXER(l_TextureMap, "number Slot", "Gets or sets a texture slot by 1-based slot number, in the order base, glow, specular, normal, height, misc, reflectance, ambient occlusion (the same order the flat \"textures\" handle uses within each material). " TEXTURE_MAP_SET_DOC, "texture", "Texture, or invalid texture handle if the handle or the slot number is invalid") { - texture_map_h *tmh = NULL; - texture_h* new_tex = nullptr; - if(!ade_get_args(L, "o|o", l_TextureMap.GetPtr(&tmh), l_Texture.GetPtr(&new_tex))) + texture_map_h *tmh = nullptr; + int slot = -1; + texture_h *new_tex = nullptr; + if (!ade_get_args(L, "oi|o", l_TextureMap.GetPtr(&tmh), &slot, l_Texture.GetPtr(&new_tex))) return ade_set_error(L, "o", l_Texture.Set(texture_h())); - texture_map *tmap = tmh->Get(); - if(tmap == NULL) + if (slot < 1 || slot > TM_NUM_TYPES) return ade_set_error(L, "o", l_Texture.Set(texture_h())); - if (ADE_SETTING_VAR && new_tex != nullptr && new_tex->isValid()) { - tmap->textures[TM_GLOW_TYPE].SetTexture(new_tex->handle, true); - } + return texture_map_slot(L, tmh, slot - 1, new_tex); +} + +ADE_FUNC(__len, l_TextureMap, nullptr, "Number of texture slots in a material, i.e. " SCP_TOKEN_TO_STR(TM_NUM_TYPES), "number", "Number of slots, or 0 if handle is invalid") +{ + texture_map_h *tmh = nullptr; + if (!ade_get_args(L, "o", l_TextureMap.GetPtr(&tmh))) + return ade_set_error(L, "i", 0); - return ade_set_args(L, "o", l_Texture.Set(texture_h(tmap->textures[TM_GLOW_TYPE].GetTexture()))); + if (!tmh->isValid()) + return ade_set_error(L, "i", 0); + + return ade_set_args(L, "i", TM_NUM_TYPES); } -ADE_VIRTVAR(SpecularMap, l_TextureMap, "texture", "Specular texture", "texture", "Texture handle, or invalid texture handle if material handle is invalid") +ADE_FUNC(__eq, l_TextureMap, "texture_map, texture_map", "Checks if two handles refer to the same material of the same model or model instance", "boolean", "True if the handles are equal") { - texture_map_h *tmh = NULL; - texture_h* new_tex = nullptr; - if(!ade_get_args(L, "o|o", l_TextureMap.GetPtr(&tmh), l_Texture.GetPtr(&new_tex))) - return ade_set_error(L, "o", l_Texture.Set(texture_h())); + texture_map_h *tmh1; + texture_map_h *tmh2; + + if (!ade_get_args(L, "oo", l_TextureMap.GetPtr(&tmh1), l_TextureMap.GetPtr(&tmh2))) + return ADE_RETURN_NIL; + + if (tmh1->GetModelID() == tmh2->GetModelID() && tmh1->GetModelInstanceID() == tmh2->GetModelInstanceID() && tmh1->GetIndex() == tmh2->GetIndex()) + return ADE_RETURN_TRUE; + + return ADE_RETURN_FALSE; +} + +ADE_VIRTVAR(Index, l_TextureMap, nullptr, "1-based index of this material in the TextureMaps array. The material's first entry in the flat \"textures\" array is at (Index-1)*" SCP_TOKEN_TO_STR(TM_NUM_TYPES) "+1.", "number", "Index, or 0 if handle is invalid") +{ + texture_map_h *tmh = nullptr; + if (!ade_get_args(L, "o", l_TextureMap.GetPtr(&tmh))) + return ade_set_error(L, "i", 0); + + if (!tmh->isValid()) + return ade_set_error(L, "i", 0); + + if (ADE_SETTING_VAR) + LuaError(L, "This property is read only."); + + return ade_set_args(L, "i", tmh->GetIndex() + 1); +} + +ADE_VIRTVAR(IsTransparent, l_TextureMap, nullptr, "Whether the model marks this material as transparent (-trans)", "boolean", "true if transparent, false if not or if handle is invalid") +{ + texture_map_h *tmh = nullptr; + if (!ade_get_args(L, "o", l_TextureMap.GetPtr(&tmh))) + return ade_set_error(L, "b", false); texture_map *tmap = tmh->Get(); - if(tmap == NULL) - return ade_set_error(L, "o", l_Texture.Set(texture_h())); + if (tmap == nullptr) + return ade_set_error(L, "b", false); + + if (ADE_SETTING_VAR) + LuaError(L, "This property is read only."); + + return ade_set_args(L, "b", tmap->is_transparent); +} + +ADE_VIRTVAR(IsAmbient, l_TextureMap, nullptr, "Whether this material uses an ambient (self-illuminated) shader", "boolean", "true if ambient, false if not or if handle is invalid") +{ + texture_map_h *tmh = nullptr; + if (!ade_get_args(L, "o", l_TextureMap.GetPtr(&tmh))) + return ade_set_error(L, "b", false); + + texture_map *tmap = tmh->Get(); + if (tmap == nullptr) + return ade_set_error(L, "b", false); + + if (ADE_SETTING_VAR) + LuaError(L, "This property is read only."); + + return ade_set_args(L, "b", tmap->is_ambient); +} + +ADE_FUNC(resetToOriginal, l_TextureMap, nullptr, "Model-level handles only: restores every slot of this material to the texture loaded from the model file, releasing any references taken by script assignments. Affects every object using the model.", "boolean", "true if reset, false if the handle is invalid or is an instance-level handle") +{ + texture_map_h *tmh = nullptr; + if (!ade_get_args(L, "o", l_TextureMap.GetPtr(&tmh))) + return ADE_RETURN_NIL; + + texture_map *tmap = tmh->Get(); + if (tmap == nullptr || tmh->isInstance()) + return ADE_RETURN_FALSE; + + tmap->ResetToOriginal(); + + return ADE_RETURN_TRUE; +} + +ADE_FUNC(resetToModel, l_TextureMap, nullptr, "Instance-level handles only: clears the " SCP_TOKEN_TO_STR(TM_NUM_TYPES) " replacement textures of this material, so that the instance draws the model's textures again. Note that instances which were assigned each other's Textures share one set of replacement textures, so this affects those instances too.", "boolean", "true if cleared, false if the handle is invalid or is a model-level handle") +{ + texture_map_h *tmh = nullptr; + if (!ade_get_args(L, "o", l_TextureMap.GetPtr(&tmh))) + return ADE_RETURN_NIL; + + polymodel_instance *pmi = tmh->GetModelInstance(); + if (pmi == nullptr) + return ADE_RETURN_FALSE; + + if (pmi->texture_replace != nullptr) + { + for (int i = 0; i < TM_NUM_TYPES; i++) + pmi->texture_replace->clear(tmh->GetIndex() * TM_NUM_TYPES + i); + } + + return ADE_RETURN_TRUE; +} + + +//**********HANDLE: texturemaps +ADE_OBJ(l_ModelTextureMaps, model_h, "texturemaps", "Array of a model's materials, as model-level texture_map handles. Entry N corresponds to entries (N-1)*" SCP_TOKEN_TO_STR(TM_NUM_TYPES) "+1 through N*" SCP_TOKEN_TO_STR(TM_NUM_TYPES) " of the model's flat \"textures\" handle."); + +ADE_FUNC(__len, l_ModelTextureMaps, nullptr, "Number of materials on the model", "number", "Number of materials, or 0 if handle is invalid") +{ + model_h *mdl = nullptr; + if (!ade_get_args(L, "o", l_ModelTextureMaps.GetPtr(&mdl))) + return ade_set_error(L, "i", 0); + + polymodel *pm = mdl->Get(); + if (pm == nullptr) + return ade_set_error(L, "i", 0); + + return ade_set_args(L, "i", pm->n_textures); +} - if (ADE_SETTING_VAR && new_tex != nullptr && new_tex->isValid()) { - tmap->textures[TM_SPECULAR_TYPE].SetTexture(new_tex->handle, true); +ADE_INDEXER(l_ModelTextureMaps, "number/string IndexOrTextureFilename", "Gets a material by 1-based index, or by the filename of the texture in any of its slots", "texture_map", "Material handle, or invalid texture_map handle if the model handle is invalid or nothing matches") +{ + model_h *mdl = nullptr; + int index = -1; + + if (lua_isnumber(L, 2)) + { + if (!ade_get_args(L, "oi", l_ModelTextureMaps.GetPtr(&mdl), &index)) + return ade_set_error(L, "o", l_TextureMap.Set(texture_map_h())); + + index--; // Lua -> FS2 + } + else + { + const char *name = nullptr; + if (!ade_get_args(L, "os", l_ModelTextureMaps.GetPtr(&mdl), &name)) + return ade_set_error(L, "o", l_TextureMap.Set(texture_map_h())); + + int slot = model_find_texture_slot(mdl->Get(), name); + if (slot < 0) + return ade_set_error(L, "o", l_TextureMap.Set(texture_map_h())); + + index = slot / TM_NUM_TYPES; } - return ade_set_args(L, "o", l_Texture.Set(texture_h(tmap->textures[TM_SPECULAR_TYPE].GetTexture()))); + polymodel *pm = mdl->Get(); + if (pm == nullptr || index < 0 || index >= pm->n_textures) + return ade_set_error(L, "o", l_TextureMap.Set(texture_map_h())); + + if (ADE_SETTING_VAR) + LuaError(L, "Assigning texture maps is not supported"); + + return ade_set_args(L, "o", l_TextureMap.Set(texture_map_h(pm, index))); } + +//**********HANDLE: modelinstancetexturemaps +ADE_OBJ(l_ModelInstanceTextureMaps, modelinstance_h, "modelinstancetexturemaps", "Array of a model instance's materials, as instance-level texture_map handles. It has the same layout as the model's \"texturemaps\" array; see also the flat \"modelinstancetextures\" handle."); + +ADE_FUNC(__len, l_ModelInstanceTextureMaps, nullptr, "Number of materials on the model instance", "number", "Number of materials, or 0 if handle is invalid") +{ + modelinstance_h *mih = nullptr; + if (!ade_get_args(L, "o", l_ModelInstanceTextureMaps.GetPtr(&mih))) + return ade_set_error(L, "i", 0); + + polymodel *pm = mih->GetModel(); + if (pm == nullptr) + return ade_set_error(L, "i", 0); + + return ade_set_args(L, "i", pm->n_textures); +} + +ADE_INDEXER(l_ModelInstanceTextureMaps, "number/string IndexOrTextureFilename", "Gets a material by 1-based index, or by texture filename. A filename is matched first against this instance's replacement textures, then against the texture in any slot of any material of the model.", "texture_map", "Material handle, or invalid texture_map handle if the model instance handle is invalid or nothing matches") +{ + modelinstance_h *mih = nullptr; + int index = -1; + + if (lua_isnumber(L, 2)) + { + if (!ade_get_args(L, "oi", l_ModelInstanceTextureMaps.GetPtr(&mih), &index)) + return ade_set_error(L, "o", l_TextureMap.Set(texture_map_h())); + + index--; // Lua -> FS2 + } + else + { + const char *name = nullptr; + if (!ade_get_args(L, "os", l_ModelInstanceTextureMaps.GetPtr(&mih), &name)) + return ade_set_error(L, "o", l_TextureMap.Set(texture_map_h())); + + int slot = model_instance_find_texture_slot(mih->Get(), mih->GetModel(), name); + if (slot < 0) + return ade_set_error(L, "o", l_TextureMap.Set(texture_map_h())); + + index = slot / TM_NUM_TYPES; + } + + polymodel_instance *pmi = mih->Get(); + polymodel *pm = mih->GetModel(); + if (pmi == nullptr || pm == nullptr || index < 0 || index >= pm->n_textures) + return ade_set_error(L, "o", l_TextureMap.Set(texture_map_h())); + + if (ADE_SETTING_VAR) + LuaError(L, "Assigning texture maps is not supported"); + + return ade_set_args(L, "o", l_TextureMap.Set(texture_map_h(pmi, index))); } + } diff --git a/code/scripting/api/objs/texturemap.h b/code/scripting/api/objs/texturemap.h index fcbef23a266..19fb8cacfae 100644 --- a/code/scripting/api/objs/texturemap.h +++ b/code/scripting/api/objs/texturemap.h @@ -4,43 +4,56 @@ #include "scripting/ade.h" #include "scripting/ade_api.h" #include "model.h" -#include "object/object.h" +#include "modelinstance.h" #include "model/model.h" -namespace scripting { -namespace api { +namespace scripting::api +{ + +// Finds the texture with the given filename on a model. Returns the flat slot index (texture_map index * TM_NUM_TYPES + slot type), +// as used by the "textures" Lua handle, or -1 if not found. +int model_find_texture_slot(polymodel *pm, const char *name); -const int THT_INDEPENDENT = 0; -const int THT_OBJECT = 1; -const int THT_MODEL = 2; +// As above, but checks the instance's replacement textures first, as the "modelinstancetextures" Lua handle does. +int model_instance_find_texture_slot(polymodel_instance *pmi, polymodel *pm, const char *name); +// A handle to one texture_map (material) of a model, or of a model instance. A model-level handle reads and writes the +// polymodel's own texture_info slots; an instance-level handle reads and writes the instance's replacement textures. class texture_map_h { protected: - int type; - object_h obj; - model_h mdl; - - texture_map *tmap; //Pointer to subsystem, or NULL for the hull + int model_num; // polymodel::id, as in model_h + int pmi_id; // polymodel_instance::id, or -1 for a model-level handle + int map_index; // index into polymodel::maps public: texture_map_h(); + explicit texture_map_h(polymodel *pm, int n_map_index); + explicit texture_map_h(polymodel_instance *pmi, int n_map_index); - texture_map_h(object *objp, texture_map *n_tmap = NULL); - - texture_map_h(int modelnum, texture_map *n_tmap = NULL); + polymodel *GetModel() const; + polymodel_instance *GetModelInstance() const; // nullptr for a model-level handle + texture_map *Get() const; - texture_map_h(polymodel *n_model, texture_map *n_tmap = NULL); + int GetModelID() const; + int GetModelInstanceID() const; // -1 for a model-level handle + int GetIndex() const; // 0-based - texture_map *Get(); + bool isInstance() const; - int GetSize(); + // These follow the same rules as the flat "textures" and "modelinstancetextures" Lua handles: at model level they read and + // write the shared model (taking a reference to the texture); at instance level they read the replacement texture if one + // is set (otherwise the model's texture) and write replacement textures. Setting an invalid handle blanks the slot at + // model level and clears the replacement at instance level. + int GetSlotTexture(int slot) const; + void SetSlotTexture(int slot, int bm_handle) const; bool isValid() const; }; DECLARE_ADE_OBJ(l_TextureMap, texture_map_h); -} -} +DECLARE_ADE_OBJ(l_ModelTextureMaps, model_h); +DECLARE_ADE_OBJ(l_ModelInstanceTextureMaps, modelinstance_h); +} diff --git a/code/scripting/lua.cpp b/code/scripting/lua.cpp index 6a773ea8990..22935a7f52c 100644 --- a/code/scripting/lua.cpp +++ b/code/scripting/lua.cpp @@ -62,6 +62,7 @@ extern "C" { #include "scripting/api/objs/subsystem.h" #include "scripting/api/objs/team.h" #include "scripting/api/objs/texture.h" +#include "scripting/api/objs/texture_replacement.h" #include "scripting/api/objs/texturemap.h" #include "scripting/api/objs/time_obj.h" #include "scripting/api/objs/vecmath.h" diff --git a/code/source_groups.cmake b/code/source_groups.cmake index 04734bca905..4e72af10269 100644 --- a/code/source_groups.cmake +++ b/code/source_groups.cmake @@ -1620,6 +1620,8 @@ add_file_folder("Scripting\\\\Api\\\\Objs" scripting/api/objs/techroom.h scripting/api/objs/texture.cpp scripting/api/objs/texture.h + scripting/api/objs/texture_replacement.cpp + scripting/api/objs/texture_replacement.h scripting/api/objs/texturemap.cpp scripting/api/objs/texturemap.h scripting/api/objs/time_obj.cpp