Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,7 @@ if (USE_TOOLS)
add_executable(fonttool tools/fonttool/main.c)
add_executable(setuptool tools/setuptool/main.c tools/shared/pilot.c)
add_executable(stringparser tools/stringparser/main.c)
add_executable(paltool tools/paltool/main.c)

list(APPEND TOOL_TARGET_NAMES
bktool
Expand All @@ -357,6 +358,7 @@ if (USE_TOOLS)
chrtool
setuptool
stringparser
paltool
)
message(STATUS "Development: CLI tools enabled")
else()
Expand Down Expand Up @@ -476,6 +478,7 @@ set(DOC_FILES
resources/gamecontrollerdb/LICENSE.gamecontrollerdb
src/vendored/argtable/LICENSE.argtable3
src/vendored/zip/LICENSE.zip
src/vendored/oklab/LICENSE.oklab
)

# Installation
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ OpenOMF contains pieces of other software, which have licenses of their own:
- resources/gamecontrollerdb.txt is under [zlib license](resources/gamecontrollerdb/LICENSE.gamecontrollerdb)
- src/vendored/argtable3 is under multiple licenses, please see [LICENSE](src/vendored/argtable/LICENSE.argtable3)
- src/vendored/zip and miniz is under the MIT license, please see [LICENSE](src/vendored/zip/LICENSE.zip)
- src/vendored/oklab is under the MIT license, please see [LICENSE](src/vendored/oklab/LICENSE.oklab)

And finally, the icon resources in resources/icons fall under CC-BY 4.0 license; please see
[LICENSE](resources/icons/LICENSE) and https://creativecommons.org/licenses/by/4.0/ for details.
Expand Down
107 changes: 100 additions & 7 deletions src/formats/chr.c
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,69 @@
#include "utils/log.h"
#include "utils/random.h"

// CHR photo sprite format extension:
// Original DOS format: len(u16) + pos_x(i16) + ...
// Extended format: len_lo(u16, bit15=1) + len_hi(u16) + pos_x(i16) + ...
// The high bit of the first uword signals that a second uword follows,
// giving 31 bits of length (15 + 16). Original DOS sprites always have
// len < 32768, so bit 15 is 0 and they read as before.

#define CHR_SPRITE_LEN_EXTENDED 0x8000

static int chr_sprite_load(sd_reader *r, sd_sprite *sprite) {
uint16_t len_lo = sd_read_uword(r);
if(len_lo & CHR_SPRITE_LEN_EXTENDED) {
sprite->len = (len_lo & 0x7FFF) | ((uint32_t)sd_read_uword(r) << 15);
} else {
sprite->len = len_lo;
}
sprite->pos_x = sd_read_word(r);
sprite->pos_y = sd_read_word(r);
sprite->width = sd_read_uword(r);
sprite->height = sd_read_uword(r);
sprite->render_height = sprite->height;
sprite->render_width = sprite->width;
sprite->index = sd_read_ubyte(r);
sprite->missing = sd_read_ubyte(r);

// Copy sprite data, if there is any.
if(sprite->missing == 0 && sprite->len != 0) {
sprite->data = omf_calloc(1, sprite->len);
sd_read_buf(r, sprite->data, sprite->len);
} else {
sprite->data = NULL;
}

if(!sd_reader_ok(r)) {
return SD_FILE_PARSE_ERROR;
}
return SD_SUCCESS;
}

static int chr_sprite_save(sd_writer *w, const sd_sprite *sprite) {
if(w == NULL || sprite == NULL) {
return SD_INVALID_INPUT;
}
if(sprite->len >= 0x8000) {
// Extended format: split 32-bit len across two uwords
sd_write_uword(w, (sprite->len & 0x7FFF) | CHR_SPRITE_LEN_EXTENDED);
sd_write_uword(w, (uint16_t)(sprite->len >> 15));
} else {
// Standard format: single uword (backward compatible)
sd_write_uword(w, (uint16_t)sprite->len);
}
sd_write_word(w, sprite->pos_x);
sd_write_word(w, sprite->pos_y);
sd_write_uword(w, sprite->width);
sd_write_uword(w, sprite->height);
sd_write_ubyte(w, sprite->index);
sd_write_ubyte(w, sprite->missing);
if(!sprite->missing) {
sd_write_buf(w, sprite->data, sprite->len);
}
return SD_SUCCESS;
}

int sd_chr_create(sd_chr_file *chr) {
assert(chr != NULL);
memset(chr, 0, sizeof(sd_chr_file));
Expand Down Expand Up @@ -208,7 +271,7 @@ int sd_chr_load(sd_chr_file *chr, const path *filename) {
// Load sprite
chr->photo = omf_calloc(1, sizeof(sd_sprite));
sd_sprite_create(chr->photo);
if(sd_sprite_load(r, chr->photo) != SD_SUCCESS) {
if(chr_sprite_load(r, chr->photo) != SD_SUCCESS) {
goto error_1;
}

Expand All @@ -229,9 +292,27 @@ int sd_chr_load(sd_chr_file *chr, const path *filename) {
chr->pilot.sex = photo->sex;
chr->pilot.photo->render_width = photo->sprite->render_width;
chr->pilot.photo->render_height = photo->sprite->render_height;
memcpy(chr->portrait_custom, photo->portrait_custom, sizeof(chr->portrait_custom));
log_debug("CHR load from PIC: portrait_custom[0]=%d/%d/%d", chr->portrait_custom[0].r,
chr->portrait_custom[0].g, chr->portrait_custom[0].b);
sd_pic_free(&players);
}

// Load portrait custom colors from CHR file if present (appended after sprite).
// These are the authoritative custom colors — the CHR file is the source of
// truth for portrait_custom once a pilot has been saved. The PIC data is
// only used as initial values when the CHR file has no appended data.
long remaining = sd_reader_filesize(r) - sd_reader_pos(r);
if(remaining >= 64 * 3) {
for(int c = 0; c < 64; c++) {
chr->portrait_custom[c].r = sd_read_ubyte(r);
chr->portrait_custom[c].g = sd_read_ubyte(r);
chr->portrait_custom[c].b = sd_read_ubyte(r);
}
log_debug("CHR load from file: portrait_custom[0]=%d/%d/%d", chr->portrait_custom[0].r,
chr->portrait_custom[0].g, chr->portrait_custom[0].b);
}

// Load colors from other files
sd_pilot_set_player_color(&chr->pilot, PRIMARY, chr->pilot.color_1);
sd_pilot_set_player_color(&chr->pilot, SECONDARY, chr->pilot.color_2);
Expand Down Expand Up @@ -289,15 +370,27 @@ int sd_chr_save(const sd_chr_file *chr, const path *filename) {
// Save this, whatever this is.
sd_write_udword(w, chr->unknown_b);

// Save photo. Hacky size fix.
chr->photo->width--;
chr->photo->height--;
// save chr->pilot.photo not chr->photo in case they're desynced
chr->pilot.photo->width--;
chr->pilot.photo->height--;

if(SD_SUCCESS != sd_sprite_save(w, chr->photo)) {
if(SD_SUCCESS != chr_sprite_save(w, chr->pilot.photo)) {
return SD_FILE_WRITE_ERROR;
}
chr->photo->width++;
chr->photo->height++;
chr->pilot.photo->width++;
chr->pilot.photo->height++;

// Save portrait custom colors (64 entries for 0x60-0x9F range)
// These are needed to correctly display mod portraits when the mod
// isn't active. Written as 64 * 3 bytes (RGB, no padding).
log_debug("CHR save: portrait_custom[0]=%d/%d/%d, [1]=%d/%d/%d", chr->portrait_custom[0].r,
chr->portrait_custom[0].g, chr->portrait_custom[0].b, chr->portrait_custom[1].r,
chr->portrait_custom[1].g, chr->portrait_custom[1].b);
for(int c = 0; c < 64; c++) {
sd_write_ubyte(w, chr->portrait_custom[c].r);
sd_write_ubyte(w, chr->portrait_custom[c].g);
sd_write_ubyte(w, chr->portrait_custom[c].b);
}

// Close & return
sd_writer_close(w);
Expand Down
1 change: 1 addition & 0 deletions src/formats/chr.h
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ typedef struct {
vga_palette pal; ///< Pilot palette
uint32_t unknown_b; ///< Unkown value. Maybe tells if there is photo data ?
sd_sprite *photo; ///< Pilot photo
vga_color portrait_custom[64]; ///< Custom portrait colors (0x60-0x9F range)
float winnings_multiplier; ///< Tournament winnings multiplier
sd_chr_enemy *enemies[MAX_CHR_ENEMIES]; ///< List of enemy states in current tournament
char bk_name[14]; ///< cutscene bk for end of tournament
Expand Down
12 changes: 7 additions & 5 deletions src/formats/pic.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,13 @@
* @todo find out what the unk_flag field is.
*/
typedef struct {
int is_player; ///< Is a player ? 1 = yes, 0 = no.
int sex; ///< Sex of person in photo. 1 = Female, 0 = Male.
vga_palette pal; ///< Image palette.
sd_sprite *sprite; ///< Photo sprite
uint8_t unk_flag; ///< Unknown flag.
int is_player; ///< Is a player ? 1 = yes, 0 = no.
int sex; ///< Sex of person in photo. 1 = Female, 0 = Male.
vga_palette pal; ///< Image palette.
sd_sprite *sprite; ///< Photo sprite
uint8_t unk_flag; ///< Unknown flag.
vga_color portrait_custom[64]; ///< Custom portrait colors at 0x60-0x9F, remapped to slot zone at display time.
///< Paltool packs 64 custom colors here.
} sd_pic_photo;

/** @brief PIC pilot portrait list
Expand Down
55 changes: 51 additions & 4 deletions src/game/gui/portrait.c
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
#include "resources/sprite.h"
#include "utils/allocator.h"
#include "utils/log.h"
#include "video/vga_extended_palette.h"
#include "video/vga_state.h"
#include "video/video.h"

// Local small gauge type
Expand All @@ -32,6 +34,13 @@ static void portrait_free(component *c) {
}

int portrait_load(sd_sprite *s, vga_palette *pal, int pilot_id) {
return portrait_load_with_slot(s, pal, pilot_id, 0, NULL);
}

int portrait_load_with_slot(sd_sprite *s, vga_palette *pal, int pilot_id, int slot_index,
vga_color portrait_custom_out[64]) {
log_debug("portrait_load_with_slot: pilot_id=%d slot=%d output=%p", pilot_id, slot_index,
(void *)portrait_custom_out);
const path filename = get_resource_filename(get_resource_file(PIC_PLAYERS));

// Load PIC file and make a surface
Expand All @@ -51,14 +60,38 @@ int portrait_load(sd_sprite *s, vga_palette *pal, int pilot_id) {
// Create new
const sd_pic_photo *photo = sd_pic_get(&pics, pilot_id);
sd_sprite_copy(s, photo->sprite);

// For mod portraits, overwrite the first 48 expanded common entries
// with the portrait's own base colors from the PIC file. This way
// the remap sends 0x00-0x2F to 0x24C+ where the portrait's colors are.
// For original portraits, the sprite doesn't reference 0x00-0x2F
// (illegal indices), so the hardcoded expanded common colors are fine.
palette_copy(pal, &photo->pal, 0, 48);

// Copy custom portrait colors into extended palette at the selected slot
for(int c = 0; c < 64; c++) {
vga_state_set_base_palette_index(VGA_EXT_SLOT1_START + (slot_index * VGA_EXT_SLOT_SIZE) + c,
&photo->portrait_custom[c]);
}

// Copy custom colors to output buffer for caller to persist (e.g. in chr->portrait_custom)
if(portrait_custom_out) {
memcpy(portrait_custom_out, photo->portrait_custom, 64 * sizeof(vga_color));
log_debug("portrait_load: copied custom colors to output (custom[0]=%d/%d/%d)", portrait_custom_out[0].r,
portrait_custom_out[0].g, portrait_custom_out[0].b);
}

// Free pics
sd_pic_free(&pics);

return SD_SUCCESS;
}

void portrait_select(component *c, int pilot_id) {
portrait_select_with_slot(c, pilot_id, 0);
}

void portrait_select_with_slot(component *c, int pilot_id, int slot_index) {
portrait *local = widget_get_obj(c);

// Free old image
Expand All @@ -71,9 +104,11 @@ void portrait_select(component *c, int pilot_id) {
sd_sprite spr;
sd_sprite_create(&spr);
vga_palette pal;
portrait_load(&spr, &pal, pilot_id);
portrait_load_with_slot(&spr, &pal, pilot_id, slot_index, NULL);

sprite_create(local->img, &spr, -1);
int sprite_remap_type = SPRITE_REMAP_PORTRAIT_1 + slot_index;
const vga_remap_table *remap = vga_extended_palette_get_sprite_remap(sprite_remap_type);
sprite_create(local->img, &spr, -1, remap);
sd_sprite_free(&spr);

// Position and size hints for the gui component
Expand Down Expand Up @@ -108,7 +143,7 @@ int portrait_selected(component *c) {
return local->selected;
}

void portrait_set_from_sprite(component *c, sd_sprite *spr) {
void portrait_set_from_sprite(component *c, sd_sprite *spr, int slot_index, const vga_color *portrait_custom) {
portrait *local = widget_get_obj(c);
// Free old image
if(local->img != NULL) {
Expand All @@ -118,7 +153,19 @@ void portrait_set_from_sprite(component *c, sd_sprite *spr) {

local->img = omf_calloc(1, sizeof(sprite));

sprite_create(local->img, spr, -1);
int sprite_remap_type = SPRITE_REMAP_PORTRAIT_1 + slot_index;
const vga_remap_table *remap = vga_extended_palette_get_sprite_remap(sprite_remap_type);
sprite_create(local->img, spr, -1, remap);

#ifdef USE_EXTENDED_PALETTE
// Copy custom portrait colors into extended palette at the selected slot
if(portrait_custom) {
for(int i = 0; i < 64; i++) {
vga_state_set_base_palette_index(VGA_EXT_SLOT1_START + (slot_index * VGA_EXT_SLOT_SIZE) + i,
&portrait_custom[i]);
}
}
#endif
component_set_size_hints(c, local->img->data->w, local->img->data->h);
}

Expand Down
22 changes: 21 additions & 1 deletion src/game/gui/portrait.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#ifndef PORTRAIT_H
#define PORTRAIT_H

#include "formats/palette.h"
#include "formats/sprite.h"
#include "game/gui/component.h"

Expand All @@ -27,6 +28,14 @@ component *portrait_create(int pilot_id);
*/
void portrait_select(component *c, int pilot_id);

/**
* @brief Select a portrait by PIC and pilot ID with extended palette slot
* @param c Portrait component to modify
* @param pilot_id Pilot index within the PIC file
* @param slot_index Portrait slot (0-4) for extended palette remapping
*/
void portrait_select_with_slot(component *c, int pilot_id, int slot_index);

/**
* @brief Get the number of pilots in a PIC file
* @param c Portrait component to query
Expand All @@ -43,6 +52,17 @@ int portrait_get_pilot_count(component *c);
*/
int portrait_load(sd_sprite *s, vga_palette *pal, int pilot_id);

/**
* @brief Load a portrait sprite and palette with extended palette slot assignment
* @param s Output sprite structure
* @param pal Output palette structure
* @param pilot_id Pilot index within the PIC file
* @param slot_index Portrait slot (0-4) for extended palette remapping
* @return Zero on success, non-zero on failure
*/
int portrait_load_with_slot(sd_sprite *s, vga_palette *pal, int pilot_id, int slot_index,
vga_color portrait_custom_out[64]);

/**
* @brief Select the next portrait
* @param c Portrait component to modify
Expand All @@ -67,6 +87,6 @@ int portrait_selected(component *c);
* @param c Portrait component to modify
* @param spr Sprite to display
*/
void portrait_set_from_sprite(component *c, sd_sprite *spr);
void portrait_set_from_sprite(component *c, sd_sprite *spr, int slot_index, const vga_color *portrait_custom);

#endif // PORTRAIT_H
6 changes: 3 additions & 3 deletions src/game/gui/trnselect.c
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ void trnselect_next(component *c) {
vga_state_set_base_palette_from_range(&trn->pal, 128, 128, 40);
load_description(&local->label, component_get_theme(c), trn->locales[0]);
sprite_free(local->img);
sprite_create(local->img, logo, -1);
sprite_create(local->img, logo, -1, NULL);
}

void trnselect_prev(component *c) {
Expand All @@ -100,7 +100,7 @@ void trnselect_prev(component *c) {
vga_state_set_base_palette_from_range(&trn->pal, 128, 128, 40);
load_description(&local->label, component_get_theme(c), trn->locales[0]);
sprite_free(local->img);
sprite_create(local->img, logo, -1);
sprite_create(local->img, logo, -1, NULL);
}

sd_tournament_file *trnselect_selected(component *c) {
Expand All @@ -124,7 +124,7 @@ static void trnselect_init(component *c, const gui_theme *theme) {
vga_state_set_base_palette_from_range(&trn->pal, 128, 128, 40);
load_description(&local->label, theme, trn->locales[0]);

sprite_create(local->img, logo, -1);
sprite_create(local->img, logo, -1, NULL);
}

component *trnselect_create(void) {
Expand Down
Loading
Loading