/* * This file is part of the DXX-Rebirth project . * It is copyright by its individual contributors, as recorded in the * project's Git history. See COPYING.txt at the top level for license * terms and a link to the Git history. */ /* * Load custom textures & robot data */ #include #include #include "gr.h" #include "pstypes.h" #include "piggy.h" #include "textures.h" #include "robot.h" #include "weapon.h" #include "digi.h" #include "hash.h" #include "u_mem.h" #include "custom.h" #include "physfsx.h" #include "compiler-range_for.h" #include "d_zip.h" #include "partial_range.h" #include #include //#define D2TMAP_CONV // used for testing namespace { struct snd_info { unsigned int length; uint8_t *data; }; } struct DiskBitmapHeader2 { char name[8]; ubyte dflags; ubyte width; ubyte height; ubyte hi_wh; ubyte flags; ubyte avg_color; int offset; } __pack__; struct DiskBitmapHeader { char name[8]; ubyte dflags; ubyte width; ubyte height; ubyte flags; ubyte avg_color; int offset; } __pack__; struct DiskSoundHeader { char name[8]; int length; int data_length; int offset; } __pack__; namespace { struct custom_info { int offset; int repl_idx; // 0x80000000 -> sound, -1 -> n/a unsigned int flags; int width, height; }; struct bitmap_original { /* Reuse the type grs_bitmap since it covers most of what is needed. * However, instances of bitmap_original are not suitable for direct * use in places where a grs_bitmap is expected. */ grs_bitmap b; }; constexpr std::integral_constant BM_FLAG_CUSTOMIZED{}; } static std::array BitmapOriginal; static std::array SoundOriginal; static int load_pig1(PHYSFS_File *f, unsigned num_bitmaps, unsigned num_sounds, unsigned &num_custom, std::unique_ptr &ci) { int data_ofs; int i; DiskBitmapHeader bmh; DiskSoundHeader sndh; char name[15]; num_custom = 0; if (num_bitmaps <= MAX_BITMAP_FILES) // 0 && num_bitmaps < PHYSFS_fileLength(f)) // >=v1.4 pig? { PHYSFSX_fseek(f, num_bitmaps, SEEK_SET); data_ofs = num_bitmaps + 8; num_bitmaps = PHYSFSX_readInt(f); num_sounds = PHYSFSX_readInt(f); } else return -1; // invalid pig file if (num_bitmaps >= MAX_BITMAP_FILES || num_sounds >= MAX_SOUND_FILES) return -1; // invalid pig file ci = std::make_unique(num_bitmaps + num_sounds); custom_info *cip = ci.get(); data_ofs += num_bitmaps * sizeof(DiskBitmapHeader) + num_sounds * sizeof(DiskSoundHeader); i = num_bitmaps; while (i--) { if (PHYSFS_read(f, &bmh, sizeof(DiskBitmapHeader), 1) < 1) { return -1; } snprintf(name, sizeof(name), "%.8s%c%d", bmh.name, (bmh.dflags & DBM_FLAG_ABM) ? '#' : 0, bmh.dflags & 63); cip->offset = bmh.offset + data_ofs; cip->repl_idx = hashtable_search(&AllBitmapsNames, name); cip->flags = bmh.flags & (BM_FLAG_TRANSPARENT | BM_FLAG_SUPER_TRANSPARENT | BM_FLAG_NO_LIGHTING | BM_FLAG_RLE); cip->width = bmh.width + ((bmh.dflags & DBM_FLAG_LARGE) ? 256 : 0); cip->height = bmh.height; cip++; } i = num_sounds; while (i--) { if (PHYSFS_read(f, &sndh, sizeof(DiskSoundHeader), 1) < 1) { return -1; } memcpy(name, sndh.name, 8); name[8] = 0; cip->offset = sndh.offset + data_ofs; cip->repl_idx = hashtable_search(&AllDigiSndNames, name) | 0x80000000; cip->width = sndh.length; cip++; } num_custom = num_bitmaps + num_sounds; return 0; } static int load_pog(PHYSFS_File *f, int pog_sig, int pog_ver, unsigned &num_custom, std::unique_ptr &ci) { int data_ofs; int num_bitmaps; int no_repl = 0; DiskBitmapHeader2 bmh; #ifdef D2TMAP_CONV int x, j, N_d2tmap; int *d2tmap = NULL; PHYSFS_File *f2 = NULL; if ((f2 = PHYSFSX_openReadBuffered("d2tmap.bin"))) { N_d2tmap = PHYSFSX_readInt(f2); if ((d2tmap = d_malloc(N_d2tmap * sizeof(d2tmap[0])))) for (i = 0; i < N_d2tmap; i++) d2tmap[i] = PHYSFSX_readShort(f2); PHYSFS_close(f2); } #endif num_custom = 0; if (pog_sig == 0x47495050 && pog_ver == 2) /* PPIG */ no_repl = 1; else if (pog_sig != 0x474f5044 || pog_ver != 1) /* DPOG */ return -1; // no pig2/pog file/unknown version num_bitmaps = PHYSFSX_readInt(f); ci = std::make_unique(num_bitmaps); custom_info *cip = ci.get(); data_ofs = 12 + num_bitmaps * sizeof(DiskBitmapHeader2); if (!no_repl) { for (int i = num_bitmaps; i--;) (cip++)->repl_idx = PHYSFSX_readShort(f); cip = ci.get(); data_ofs += num_bitmaps * 2; } #ifdef D2TMAP_CONV if (d2tmap) for (i = 0; i < num_bitmaps; i++) { x = cip[i].repl_idx; cip[i].repl_idx = -1; for (j = 0; j < N_d2tmap; j++) if (x == d2tmap[j]) { cip[i].repl_idx = Textures[j % NumTextures].index; break; } } #endif for (int i = num_bitmaps; i--;) { if (PHYSFS_read(f, &bmh, sizeof(DiskBitmapHeader2), 1) < 1) { return -1; } cip->offset = bmh.offset + data_ofs; cip->flags = bmh.flags & (BM_FLAG_TRANSPARENT | BM_FLAG_SUPER_TRANSPARENT | BM_FLAG_NO_LIGHTING | BM_FLAG_RLE); cip->width = bmh.width + ((bmh.hi_wh & 15) << 8); cip->height = bmh.height + ((bmh.hi_wh >> 4) << 8); cip++; } num_custom = num_bitmaps; return 0; } // load custom textures/sounds from pog/pig file // returns 0 if ok, <0 on error static int load_pigpog(const d_fname &pogname) { unsigned num_custom; grs_bitmap *bmp; digi_sound *snd; uint8_t *p; auto f = PHYSFSX_openReadBuffered(pogname).first; int i, j, rc = -1; unsigned int x = 0; if (!f) return -1; // pog file doesn't exist i = PHYSFSX_readInt(f); x = PHYSFSX_readInt(f); std::unique_ptr ci; if (load_pog(f, i, x, num_custom, ci) && load_pig1(f, i, x, num_custom, ci)) { return rc; } custom_info *cip = ci.get(); i = num_custom; while (i--) { x = cip->repl_idx; if (cip->repl_idx >= 0) { PHYSFSX_fseek( f, cip->offset, SEEK_SET ); if ( cip->flags & BM_FLAG_RLE ) j = PHYSFSX_readInt(f); else j = cip->width * cip->height; if (!MALLOC(p, ubyte, j)) { return rc; } bmp = &GameBitmaps[x]; auto &bmo = BitmapOriginal[x].b; if (bmo.get_flag_mask(BM_FLAG_CUSTOMIZED)) // already customized? /* If the bitmap was previously customized, and another * customization is loaded on top of it, discard the * first customization. Retain the stock bitmap in the * backup data structure. */ gr_free_bitmap_data(*bmp); else { /* If the bitmap was not previously customized, then * copy it to a backup data structure before replacing * it with the custom version. */ // save original bitmap info bmo = *bmp; bmo.add_flags(BM_FLAG_CUSTOMIZED); if (GameBitmapOffset[x] != pig_bitmap_offset::None) // from pig? { /* Borrow the data field to store the offset within * the pig file, which is not a pointer. Attempts * to dereference bm_data will almost certainly * crash. */ bmo.add_flags(BM_FLAG_PAGED_OUT); bmo.bm_data = reinterpret_cast(static_cast(GameBitmapOffset[x])); } } GameBitmapOffset[x] = pig_bitmap_offset::None; // not in pig *bmp = {}; gr_init_bitmap(*bmp, bm_mode::linear, 0, 0, cip->width, cip->height, cip->width, p); gr_set_bitmap_flags(*bmp, cip->flags & 255); #if !DXX_USE_OGL bmp->avg_color = cip->flags >> 8; #endif if ( cip->flags & BM_FLAG_RLE ) { int *ip = reinterpret_cast(p); *ip = j; p += 4; j -= 4; } if (PHYSFS_read(f, p, 1, j) < 1) { return rc; } } else if ((cip->repl_idx + 1) < 0) { PHYSFSX_fseek( f, cip->offset, SEEK_SET ); snd = &GameSounds[x & 0x7fffffff]; j = cip->width; if (!MALLOC(p, ubyte, j)) { return rc; } if (SoundOriginal[x & 0x7fffffff].length & 0x80000000) // already customized? d_free(snd->data); else { #ifdef ALLEGRO SoundOriginal[x & 0x7fffffff].length = snd->len | 0x80000000; #else SoundOriginal[x & 0x7fffffff].length = snd->length | 0x80000000; #endif SoundOriginal[x & 0x7fffffff].data = snd->data; } #ifdef ALLEGRO snd->loop_end = snd->len = j; #else snd->length = j; #endif snd->data = p; if (PHYSFS_read(f, p, j, 1) < 1) { return rc; } } cip++; } rc = 0; return rc; } static int read_d2_robot_info(PHYSFS_File *fp, robot_info &ri) { int j; ri.model_num = PHYSFSX_readInt(fp); for (auto &j : ri.gun_points) PHYSFSX_readVector(fp, j); for (auto &j : ri.gun_submodels) j = PHYSFSX_readByte(fp); ri.exp1_vclip_num = PHYSFSX_readShort(fp); ri.exp1_sound_num = PHYSFSX_readShort(fp); ri.exp2_vclip_num = PHYSFSX_readShort(fp); ri.exp2_sound_num = PHYSFSX_readShort(fp); const auto weapon_type = PHYSFSX_readByte(fp); ri.weapon_type = weapon_type < N_weapon_types ? static_cast(weapon_type) : weapon_id_type::LASER_ID_L1; /*ri.weapon_type2 =*/ PHYSFSX_readByte(fp); ri.n_guns = PHYSFSX_readByte(fp); ri.contains_id = PHYSFSX_readByte(fp); ri.contains_count = PHYSFSX_readByte(fp); ri.contains_prob = PHYSFSX_readByte(fp); ri.contains_type = PHYSFSX_readByte(fp); /*ri.kamikaze =*/ PHYSFSX_readByte(fp); ri.score_value = PHYSFSX_readShort(fp); /*ri.badass =*/ PHYSFSX_readByte(fp); /*ri.energy_drain =*/ PHYSFSX_readByte(fp); ri.lighting = PHYSFSX_readFix(fp); ri.strength = PHYSFSX_readFix(fp); ri.mass = PHYSFSX_readFix(fp); ri.drag = PHYSFSX_readFix(fp); for (auto &j : ri.field_of_view) j = PHYSFSX_readFix(fp); for (auto &j : ri.firing_wait) j = PHYSFSX_readFix(fp); for (j = 0; j < NDL; j++) /*ri.firing_wait2[j] =*/ PHYSFSX_readFix(fp); for (auto &j : ri.turn_time) j = PHYSFSX_readFix(fp); #if 0 // not used in d1, removed in d2 for (j = 0; j < NDL; j++) ri.fire_power[j] = PHYSFSX_readFix(fp); for (j = 0; j < NDL; j++) ri.shield[j] = PHYSFSX_readFix(fp); #endif for (auto &j : ri.max_speed) j = PHYSFSX_readFix(fp); for (auto &j : ri.circle_distance) j = PHYSFSX_readFix(fp); for (auto &j : ri.rapidfire_count) j = PHYSFSX_readByte(fp); for (auto &j : ri.evade_speed) j = PHYSFSX_readByte(fp); ri.cloak_type = PHYSFSX_readByte(fp); ri.attack_type = PHYSFSX_readByte(fp); ri.see_sound = PHYSFSX_readByte(fp); ri.attack_sound = PHYSFSX_readByte(fp); ri.claw_sound = PHYSFSX_readByte(fp); /*ri.taunt_sound =*/ PHYSFSX_readByte(fp); ri.boss_flag = PHYSFSX_readByte(fp); /*ri.companion =*/ PHYSFSX_readByte(fp); /*ri.smart_blobs =*/ PHYSFSX_readByte(fp); /*ri.energy_blobs =*/ PHYSFSX_readByte(fp); /*ri.thief =*/ PHYSFSX_readByte(fp); /*ri.pursuit =*/ PHYSFSX_readByte(fp); /*ri.lightcast =*/ PHYSFSX_readByte(fp); /*ri.death_roll =*/ PHYSFSX_readByte(fp); /*ri.flags =*/ PHYSFSX_readByte(fp); /*ri.pad[0] =*/ PHYSFSX_readByte(fp); /*ri.pad[1] =*/ PHYSFSX_readByte(fp); /*ri.pad[2] =*/ PHYSFSX_readByte(fp); /*ri.deathroll_sound =*/ PHYSFSX_readByte(fp); /*ri.glow =*/ PHYSFSX_readByte(fp); /*ri.behavior =*/ PHYSFSX_readByte(fp); /*ri.aim =*/ PHYSFSX_readByte(fp); for (auto &j : ri.anim_states) { for (auto &k : j) { k.n_joints = PHYSFSX_readShort(fp); k.offset = PHYSFSX_readShort(fp); } } ri.always_0xabcd = PHYSFSX_readInt(fp); return 1; } namespace dsx { static void load_hxm(const d_fname &hxmname) { auto &Robot_joints = LevelSharedRobotJointState.Robot_joints; unsigned int repl_num; int i; auto f = PHYSFSX_openReadBuffered(hxmname).first; int n_items; if (!f) return; // hxm file doesn't exist if (PHYSFSX_readInt(f) != 0x21584d48) /* HMX! */ { // invalid hxm file return; } if (PHYSFSX_readInt(f) != 1) { // unknown version return; } // read robot info if ((n_items = PHYSFSX_readInt(f)) != 0) { auto &Robot_info = LevelSharedRobotInfoState.Robot_info; for (i = 0; i < n_items; i++) { repl_num = PHYSFSX_readInt(f); if (repl_num >= MAX_ROBOT_TYPES) { PHYSFSX_fseek(f, 480, SEEK_CUR); /* sizeof d2_robot_info */ } else { if (!(read_d2_robot_info(f, Robot_info[repl_num]))) { return; } } } } // read joint positions if ((n_items = PHYSFSX_readInt(f)) != 0) { for (i = 0; i < n_items; i++) { repl_num = PHYSFSX_readInt(f); if (repl_num >= MAX_ROBOT_JOINTS) PHYSFSX_fseek(f, sizeof(jointpos), SEEK_CUR); else { jointpos_read(f, Robot_joints[repl_num]); } } } // read polygon models if ((n_items = PHYSFSX_readInt(f)) != 0) { for (i = 0; i < n_items; i++) { polymodel *pm; repl_num = PHYSFSX_readInt(f); if (repl_num >= MAX_POLYGON_MODELS) { PHYSFSX_readInt(f); // skip n_models PHYSFSX_fseek(f, 734 - 8 + PHYSFSX_readInt(f) + 8, SEEK_CUR); } else { auto &Polygon_models = LevelSharedPolygonModelState.Polygon_models; pm = &Polygon_models[repl_num]; polymodel_read(*pm, f); const auto model_data_size = pm->model_data_size; pm->model_data = std::make_unique(model_data_size); if (PHYSFS_read(f, pm->model_data, model_data_size, 1) < 1) { pm->model_data.reset(); return; } Dying_modelnums[repl_num] = PHYSFSX_readInt(f); Dead_modelnums[repl_num] = PHYSFSX_readInt(f); } } } // read object bitmaps if ((n_items = PHYSFSX_readInt(f)) != 0) { for (i = 0; i < n_items; i++) { const auto oi = static_cast(PHYSFSX_readInt(f)); auto v = PHYSFSX_readShort(f); if (ObjBitmaps.valid_index(oi)) ObjBitmaps[oi].index = v; } } } } // undo customized items static void custom_remove() { int i; for (auto &&[gbo, bmo, bmp] : zip(GameBitmapOffset, BitmapOriginal, GameBitmaps)) if (bmo.b.get_flag_mask(BM_FLAG_CUSTOMIZED)) { gr_free_bitmap_data(bmp); bmp = bmo.b; bmo.b.clear_flags(); if (bmp.get_flag_mask(BM_FLAG_PAGED_OUT)) { /* If the bitmap was unloaded before it was customized, * then restore GameBitmapOffset so that the stock data * can be reloaded when it is next needed. */ /* Casting this pointer into a smaller integer is legal * because the value in bm_data is not a pointer. It is * a copy of the value that GameBitmapOffset had when * the bitmap_original was initialized. */ gbo = static_cast(reinterpret_cast(bmo.b.bm_data)); gr_set_bitmap_data(bmp, nullptr); } else { /* Otherwise, the bitmap was loaded before it was * overwritten, so the copy restored from the backup is * ready for use. */ bmp.set_flags(bmp.get_flags() & ~BM_FLAG_CUSTOMIZED); } } for (i = 0; i < MAX_SOUND_FILES; i++) if (SoundOriginal[i].length & 0x80000000) { d_free(GameSounds[i].data); GameSounds[i].data = SoundOriginal[i].data; #ifdef ALLEGRO GameSounds[i].len = SoundOriginal[i].length & 0x7fffffff; #else GameSounds[i].length = SoundOriginal[i].length & 0x7fffffff; #endif SoundOriginal[i].length = 0; } } void load_custom_data(const d_fname &level_name) { custom_remove(); d_fname custom_file; using std::begin; using std::end; using std::copy; using std::next; auto bl = begin(level_name); auto bc = begin(custom_file); auto &pg1 = ".pg1"; copy(bl, next(bl, custom_file.size() - sizeof(pg1)), bc); auto o = std::find(bc, next(bc, custom_file.size() - sizeof(pg1)), '.'); copy(begin(pg1), end(pg1), o); load_pigpog(custom_file); auto &dtx = "dtx"; ++o; copy(begin(dtx), end(dtx), o); load_pigpog(custom_file); auto &hx1 = "hx1"; copy(begin(hx1), end(hx1), o); load_hxm(custom_file); } void custom_close() { custom_remove(); }