Preserve current legacy presentation and options improvements

This commit is contained in:
raptorreplica committed 2026-10-10 11:07:37 +11:00
1 parent 214789fe87
commit eee082f2ad
15 files changed
+1090 -73

No files matched your search

+51
View File
@@ -121,6 +121,40 @@ active. Exclusive `fullscreen_width/height` modes ignore it; if both
`fullscreen=1` and `borderless=1` are set, `fullscreen` wins. The stale
commented-out `-borderless` command-line snippet was removed.
## Video: HD presentation filter (scale2x/scale3x)
New `[Video]` SETUP.INI keys, read once at boot in `VIDEO_LoadPrefs`
(`src/i_video.cpp`): `upscaler` (0 = off, 2/3/4 = scale2x/scale3x/
2x scale2x, default 3) and `scanlines` (0/1, default 0). The filter is a
presentation-level CPU pass over the final RGBA frame before it enters the
streaming texture — no engine logic or asset changes; the 4:3 playfield is
kept. Kernels are the AdvMAME scale2x/scale3x rules by Andrea Mazzoleni,
reimplemented in-repo (`src/i_upscale.cpp`, GPLv2+-compatible; nothing
vendored). Filtered frames render directly from the factor-sized streaming
texture and bypass the nearest-integer intermediate (`texture_upscaled`),
which only sharpens sub-integer fits of a 1x source. With `upscaler=0` the
original four-stage pixel-perfect path is byte-for-byte unchanged. The
scanline overlay darkens every third logical row (320x240 space) after the
final copy, so it stays geometry-safe under every window mode. Like
`Video/borderless`, these are hand-edited INI settings; `raptorsetup` is
not extended.
## Effects: machine-gun casings eject into world space
Upstream's forward-gun muzzle effect (`A_PLAYER_SHOOT`, the GUNSTR
diagonal streak art) is a player-relative anim: the streak is glued to
the wingtip and flickers in place for its 4-step life, which at the
decoupled presentation rate reads as shells vibrating at the gun rather
than being ejected. This fork registers the same art once more as
`A_PLAYER_EJECTA` (GROUND layer, `src/anims.cpp`) and spawns that at each
wingtip's world position (`SHOTS_PlayerShoot` case `S_FORWARD_GUNS`) via
`ANIMS_StartGAnim`. The casings therefore stay behind as the plane moves
on and drift down with the map scroll (1 px per simulation step, ~23 px/s)
for their 172 ms life. Sim-side timing is untouched: spawn cadence
(shoot_rate 2), anim rate, RNG consumption and demo playback are
identical to upstream; only the anim's reference frame changes.
## Input: no cursor snapping
Dialog/menu code no longer warps the mouse cursor onto the default
@@ -132,6 +166,23 @@ selection still highlights fields without moving the pointer. The
mission-start `PTR_SetPos(playerx, playery)` in `Do_Game` is kept — it
seeds mouse ship control, not menu focus.
## UI: in-game options menu with runtime video settings
The Options dialog now carries the HD-remaster video settings that were
previously boot-only INI keys. `ui/OPTS_SWD` was re-authored (same catalog
handle `0x1003f`; built by `tools/swdbuild.py` from
`tools/opts_swd_spec.py`) from 13 to 23 fields, adding FILTER, SCANLINES,
RESOLUTION, FULLSCREEN and ASPECT rows with selection icons beside the
existing Music/FX/Detail rows, and new `OPTS_*` ids in `src/winids.h`.
`src/i_video.cpp` gained runtime setters (`I_SetVideoFilter/Scanlines/
VideoResolution/Fullscreen/AspectCorrect`) that only touch presentation
resources — the factor-sized surface/texture pair was extracted from
`SetVideoMode` into `RebuildStreamTextures` so the filter can change
without re-creating the 8-bit screenbuffer. Changes live-apply from the
dialog and persist to `[Video]` SETUP.INI keys (`upscaler`, `scanlines`,
`fullscreen`, `aspect_ratio_correct`) on EXIT, and the dialog is also
reachable in-mission via the O key (`Do_Game`, bracketed like F1 help).
## Deliberately unchanged
- Game logic, mission data formats, audio synthesis — untouched.
+1
View File
@@ -35,6 +35,7 @@ set(SOURCE_FILES
src/glbapi.cpp
src/i_video.cpp
src/musapi.cpp
src/i_upscale.cpp
src/movie_a.cpp
src/ptrapi_a.cpp
src/swdapi.cpp
+3
View File
@@ -183,6 +183,9 @@ ANIMS_Init(
// SUPER SHIELD
ANIMS_Register(FILE149_SHIPGLOW_BLK, 4, HIGH_AIR, 1, 1, A_NORM);
// MACHINE-GUN EJECTA (world-space casing streaks, GUNSTR art)
ANIMS_Register(FILE1c7_GUNSTR_BLK, 4, GROUND, 0, 0, A_NORM);
}
/***************************************************************************
+5
View File
@@ -70,6 +70,11 @@ typedef struct ANIMS_S
#define A_ENERGY_GRAB 19
#define A_SUPER_SHIELD 20
// Machine-gun ejecta: same GUNSTR streak art dropped into world space at
// the wingtips (ground layer, drifts with the map scroll) so casings stay
// behind the plane instead of flicking in place at the gun.
#define A_PLAYER_EJECTA 21
void ANIMS_Clear(void);
ANIMS *ANIMS_Remove(ANIMS *anim);
void ANIMS_Init(void);
+162
View File
@@ -0,0 +1,162 @@
//
// Copyright(C) 1993-1996 Id Software, Inc.
// Copyright(C) 2005-2014 Simon Howard
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// DESCRIPTION:
// AdvMAME scale2x/scale3x filters by Andrea Mazzoleni, reimplemented
// in-repo (GPLv2+, same license as the project). Each source pixel E
// with its neighbors
//
// A B C
// D E F
// G H I
//
// expands to a 2x2 (scale2x) or 3x3 (scale3x) block of output pixels.
// Factor 4 is scale2x applied twice, the reference composition.
//
#include <stdlib.h>
#include "common.h"
#include "i_upscale.h"
// Exact per-byte average of two packed RGBA pixels: the common bits plus
// half the differing bits. The mask keeps the halving inside each byte
// lane, so no carry crosses channels and no unpacking is needed.
static uint32_t pixel_blend(uint32_t x, uint32_t y)
{
return (x & y) + (((x ^ y) >> 1) & 0x7f7f7f7f);
}
static void scale2x_pass(const uint32_t *src, int spitch, int w, int h,
uint32_t *dst, int dpitch)
{
int x, y;
for (y = 0; y < h; ++y)
{
const uint32_t *row = src + y * spitch;
const uint32_t *above = y > 0 ? row - spitch : row;
const uint32_t *below = y < h - 1 ? row + spitch : row;
uint32_t *out0 = dst + (y * 2) * dpitch;
uint32_t *out1 = dst + (y * 2 + 1) * dpitch;
for (x = 0; x < w; ++x)
{
// Out-of-bounds neighbors are treated as E (edges of the
// above/below row aliases and of the x clamp do exactly that).
uint32_t E = row[x];
uint32_t B = above[x];
uint32_t H = below[x];
uint32_t D = x > 0 ? row[x - 1] : E;
uint32_t F = x < w - 1 ? row[x + 1] : E;
uint32_t a, b, c, d;
a = (D == B && D != H && B != F) ? D : E;
b = (B == F && B != D && F != H) ? B : E;
c = (D == H && D != B && H != F) ? D : E;
d = (H == F && H != D && F != B) ? F : E;
out0[x * 2] = a;
out0[x * 2 + 1] = b;
out1[x * 2] = c;
out1[x * 2 + 1] = d;
}
}
}
static void scale3x_pass(const uint32_t *src, int spitch, int w, int h,
uint32_t *dst, int dpitch)
{
int x, y;
for (y = 0; y < h; ++y)
{
const uint32_t *row = src + y * spitch;
const uint32_t *above = y > 0 ? row - spitch : row;
const uint32_t *below = y < h - 1 ? row + spitch : row;
uint32_t *out0 = dst + (y * 3) * dpitch;
uint32_t *out1 = dst + (y * 3 + 1) * dpitch;
uint32_t *out2 = dst + (y * 3 + 2) * dpitch;
for (x = 0; x < w; ++x)
{
uint32_t E = row[x];
uint32_t B = above[x];
uint32_t H = below[x];
uint32_t D = x > 0 ? row[x - 1] : E;
uint32_t F = x < w - 1 ? row[x + 1] : E;
uint32_t a0, a1, a2, a3, a4, a5, a6, a7, a8;
a4 = E;
a1 = (B == D) ? D : E;
a5 = (B == F) ? F : E;
a7 = (H == F) ? F : E;
a3 = (H == D) ? D : E;
a0 = (B == D && B != F && D != H) ? D
: ((D == B && D == H && F != H && B != F) ? pixel_blend(D, B) : E);
a2 = (B == F && B != D && F != H) ? F
: ((B == F && H == F && D != H && B != D) ? pixel_blend(B, F) : E);
a6 = (D == H && D != B && H != F) ? D
: ((D == H && H == F && B != F && D != B) ? pixel_blend(D, H) : E);
a8 = (H == F && H != D && F != B) ? F
: ((H == F && F == B && D != B && H != D) ? pixel_blend(H, F) : E);
out0[x * 3] = a0;
out0[x * 3 + 1] = a1;
out0[x * 3 + 2] = a2;
out1[x * 3] = a3;
out1[x * 3 + 1] = a4;
out1[x * 3 + 2] = a5;
out2[x * 3] = a6;
out2[x * 3 + 1] = a7;
out2[x * 3 + 2] = a8;
}
}
}
void I_UpscaleFrame(const uint32_t *src, int srcpitch_px, int w, int h,
uint32_t *dst, int dstpitch_px, int factor)
{
if (factor == 2)
{
scale2x_pass(src, srcpitch_px, w, h, dst, dstpitch_px);
}
else if (factor == 3)
{
scale3x_pass(src, srcpitch_px, w, h, dst, dstpitch_px);
}
else if (factor == 4)
{
// scale2x applied twice (640x400x4 = 1 MB temporary).
uint32_t *tmp = (uint32_t *) malloc(sizeof(uint32_t)
* w * 2 * h * 2);
if (tmp == NULL)
{
EXIT_Error("I_UpscaleFrame: memory");
}
scale2x_pass(src, srcpitch_px, w, h, tmp, w * 2);
scale2x_pass(tmp, w * 2, w * 2, h * 2, dst, dstpitch_px);
free(tmp);
}
else
{
EXIT_Error("I_UpscaleFrame: bad factor %d", factor);
}
}
+28
View File
@@ -0,0 +1,28 @@
//
// Copyright(C) 1993-1996 Id Software, Inc.
// Copyright(C) 2005-2014 Simon Howard
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// DESCRIPTION:
// Presentation-level pixel-art upscaler (AdvMAME scale2x/scale3x).
//
#pragma once
#include <stdint.h>
// Upscale a w x h 32-bit RGBA frame by factor (2, 3 or 4) into dst, which
// must be w*factor x h*factor pixels. Pitches are in pixels, not bytes;
// the caller converts surface pitches. No SDL dependency, no global state.
void I_UpscaleFrame(const uint32_t *src, int srcpitch_px, int w, int h,
uint32_t *dst, int dstpitch_px, int factor);
+181 -12
View File
@@ -38,6 +38,7 @@
#include "musapi.h"
#include "prefapi.h"
#include "joyapi.h"
#include "i_upscale.h"
// These are (1) the window (or the full screen) that our game is rendered to
// and (2) the renderer that scales the texture (see below) into this window.
@@ -61,6 +62,12 @@ static SDL_Surface *argbbuffer = NULL;
static SDL_Texture *texture = NULL;
static SDL_Texture *texture_upscaled = NULL;
// HD remaster: when the presentation upscaler is active, the filtered
// factor-sized RGBA frame lands here before going into the streaming
// texture.
static SDL_Surface *argbbuffer_scaled = NULL;
static SDL_Rect blit_rect = {
0,
0,
@@ -117,6 +124,13 @@ int fullscreen_width = 0, fullscreen_height = 0;
static int max_scaling_buffer_pixels = 16000000;
// HD remaster: presentation-level pixel-art upscaler (0 = off, else 2/3/4)
// and scanline overlay (0 = off, 1 = on). Read from [Video] upscaler /
// [Video] scanlines in SETUP.INI at boot only; changing them needs a restart.
static int upscale_factor = 3;
static int scanlines = 0;
// Run in full screen mode? (int type for config code)
// int fullscreen = true;
@@ -218,6 +232,14 @@ void VIDEO_LoadPrefs(void)
aspect_ratio_correct = INI_GetPreferenceLong("Video", "aspect_ratio_correct", 1);
txt_fullscreen = INI_GetPreferenceLong("Video", "txt_fullscreen", 0);
borderless = INI_GetPreferenceLong("Video", "borderless", 0);
upscale_factor = INI_GetPreferenceLong("Video", "upscaler", 3);
scanlines = INI_GetPreferenceLong("Video", "scanlines", 0);
if (upscale_factor != 0 && upscale_factor != 2 && upscale_factor != 3
&& upscale_factor != 4)
{
upscale_factor = 3;
}
}
static bool MouseShouldBeGrabbed()
@@ -854,24 +876,64 @@ void I_FinishUpdate (void)
SDL_LowerBlit(screenbuffer, &blit_rect, argbbuffer, &blit_rect);
// Update the intermediate texture with the contents of the RGBA buffer.
if (upscale_factor > 1)
{
// HD remaster path: filter the RGBA frame into the factor-sized
// surface and stream it directly to screen. The source is already
// integer-scaled by the filter, so the nearest-integer intermediate
// (which exists to sharpen sub-integer fits of a 1x source) is not
// used here.
SDL_UpdateTexture(texture, NULL, argbbuffer->pixels, argbbuffer->pitch);
I_UpscaleFrame((uint32_t *) argbbuffer->pixels,
argbbuffer->pitch / 4,
SCREENWIDTH, SCREENHEIGHT,
(uint32_t *) argbbuffer_scaled->pixels,
argbbuffer_scaled->pitch / 4,
upscale_factor);
// Make sure the pillarboxes are kept clear each frame.
SDL_UpdateTexture(texture, NULL, argbbuffer_scaled->pixels,
argbbuffer_scaled->pitch);
SDL_RenderClear(renderer);
SDL_RenderClear(renderer);
SDL_SetRenderTarget(renderer, NULL);
SDL_RenderCopy(renderer, texture, NULL, NULL);
}
else
{
// Update the intermediate texture with the contents of the RGBA buffer.
// Render this intermediate texture into the upscaled texture
// using "nearest" integer scaling.
SDL_UpdateTexture(texture, NULL, argbbuffer->pixels, argbbuffer->pitch);
SDL_SetRenderTarget(renderer, texture_upscaled);
SDL_RenderCopy(renderer, texture, NULL, NULL);
// Make sure the pillarboxes are kept clear each frame.
// Finally, render this upscaled texture to screen using linear scaling.
SDL_RenderClear(renderer);
SDL_SetRenderTarget(renderer, NULL);
SDL_RenderCopy(renderer, texture_upscaled, NULL, NULL);
// Render this intermediate texture into the upscaled texture
// using "nearest" integer scaling.
SDL_SetRenderTarget(renderer, texture_upscaled);
SDL_RenderCopy(renderer, texture, NULL, NULL);
// Finally, render this upscaled texture to screen using linear scaling.
SDL_SetRenderTarget(renderer, NULL);
SDL_RenderCopy(renderer, texture_upscaled, NULL, NULL);
}
if (scanlines)
{
// Scanline overlay: every third logical row darkened. Logical size
// is 320xactualheight, so the lines stay evenly spaced physical rows
// under any window size.
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 70);
for (int y = 1; y < actualheight; y += 3)
{
SDL_RenderDrawLine(renderer, 0, y, SCREENWIDTH, y);
}
}
// Draw!
@@ -1242,6 +1304,7 @@ void I_GetWindowPosition(int *x, int *y, int w, int h)
}
}
static void RebuildStreamTextures(void);
static void SetVideoMode(void)
{
int w, h;
@@ -1452,6 +1515,33 @@ static void SetVideoMode(void)
SDL_FillRect(argbbuffer, NULL, 0);
}
// HD remaster: with the upscaler active, a factor-sized RGBA surface
// feeds the (factor-sized) streaming texture. Freed alongside
// argbbuffer above on every re-init.
RebuildStreamTextures();
}
// Rebuild the factor-sized RGBA surface and the streaming texture that
// carry the frame at the current upscale_factor. Shared by SetVideoMode
// (full re-init) and the runtime filter setter (I_SetVideoFilter).
static void RebuildStreamTextures(void)
{
if (argbbuffer_scaled != NULL)
{
SDL_FreeSurface(argbbuffer_scaled);
argbbuffer_scaled = NULL;
}
if (upscale_factor > 1)
{
argbbuffer_scaled = SDL_CreateRGBSurfaceWithFormat(0,
SCREENWIDTH * upscale_factor,
SCREENHEIGHT * upscale_factor,
32, pixel_format);
SDL_FillRect(argbbuffer_scaled, NULL, 0);
}
if (texture != NULL)
{
SDL_DestroyTexture(texture);
@@ -1470,13 +1560,92 @@ static void SetVideoMode(void)
texture = SDL_CreateTexture(renderer,
pixel_format,
SDL_TEXTUREACCESS_STREAMING,
SCREENWIDTH, SCREENHEIGHT);
upscale_factor > 1
? SCREENWIDTH * upscale_factor
: SCREENWIDTH,
upscale_factor > 1
? SCREENHEIGHT * upscale_factor
: SCREENHEIGHT);
// Initially create the upscaled texture for rendering to screen
CreateUpscaledTexture(true);
}
//**************************************************************************
// Runtime video option setters (in-game Options dialog)
//
// These only touch presentation resources. Never call SetVideoMode() from
// here: it frees and recreates screenbuffer without re-pointing
// I_VideoBuffer, so the game would keep drawing into freed memory.
//**************************************************************************
void I_SetVideoFilter(int factor)
{
if (factor != 0 && factor != 2 && factor != 3 && factor != 4)
return;
if (factor == upscale_factor)
return;
upscale_factor = factor;
RebuildStreamTextures();
}
void I_SetScanlines(int on)
{
scanlines = on ? 1 : 0;
}
void I_SetVideoResolution(int w, int h)
{
if (fullscreen)
I_SetFullscreen(0);
window_width = w;
window_height = h;
AdjustWindowSize();
SDL_SetWindowSize(screen, window_width, window_height);
// The RESIZED event path in I_FinishUpdate adapts the textures.
}
void I_SetFullscreen(int on)
{
fullscreen = on ? 1 : 0;
SDL_SetWindowFullscreen(screen,
fullscreen ? SDL_WINDOW_FULLSCREEN_DESKTOP : 0);
if (!fullscreen)
{
AdjustWindowSize();
SDL_SetWindowSize(screen, window_width, window_height);
}
}
void I_SetAspectCorrect(int on)
{
aspect_ratio_correct = on ? 1 : 0;
actualheight = aspect_ratio_correct ? SCREENHEIGHT_4_3 : SCREENHEIGHT;
SDL_RenderSetLogicalSize(renderer, SCREENWIDTH, actualheight);
AdjustWindowSize();
SDL_SetWindowSize(screen, window_width, window_height);
CreateUpscaledTexture(false);
palette_to_set = true;
}
int I_GetVideoFilter(void)
{
return upscale_factor;
}
int I_GetScanlines(void)
{
return scanlines;
}
void I_InitGraphics(uint8_t *pal)
{
SDL_Event dummy;
+10
View File
@@ -37,6 +37,7 @@ extern int window_width;
extern int window_height;
extern int fullscreen;
extern int txt_fullscreen;
extern int aspect_ratio_correct;
extern pixel_t *I_VideoBuffer;
@@ -50,6 +51,15 @@ void I_GetMousePos(int *x, int *y);
void I_SetMousePos(int x, int y);
void I_SetGrabMouseCallback(grabmouse_callback_t func);
void VIDEO_LoadPrefs(void);
// Runtime video option setters (in-game Options dialog)
void I_SetVideoFilter(int factor); // 0/2/3/4, anything else is ignored
void I_SetScanlines(int on);
void I_SetVideoResolution(int w, int h);
void I_SetFullscreen(int on);
void I_SetAspectCorrect(int on);
int I_GetVideoFilter(void);
int I_GetScanlines(void);
void closewindow(void);
bool I_GetNeedResize(bool setonlypos);
int I_VsyncActive(void);
+16 -1
View File
@@ -922,7 +922,22 @@ Do_Game(
b3_flag = 0;
}
if (KBD_IsKey(SC_P) || JOY_IsKeyInGameStart(Start))
if (KBD_IsKey(SC_O))
{
SWD_SetClearFlag(0);
IPT_End();
WIN_Opts();
IPT_Start();
RAP_ClearSides();
RAP_DisplayStats();
SWD_SetClearFlag(1);
IMS_StartAck();
BUT_1 = BUT_2 = BUT_3 = BUT_4 = 0;
b2_flag = 0;
b3_flag = 0;
}
if (KBD_IsKey(SC_P) || JOY_IsKeyInGameStart(Start))
{
while (IMS_IsAck())
{
+6 -2
View File
@@ -633,7 +633,10 @@ SHOTS_PlayerShoot(
cur->move.y2 = 0;
cur->startx = player_cx;
cur->starty = player_cy;
ANIMS_StartAnim(A_PLAYER_SHOOT, o_gun1[playerpic], 0);
// Casing streaks drop into world space (StartGAnim takes the
// top-left; the 8x8 GUNSTR art is centered on the muzzle by hand).
ANIMS_StartGAnim(A_PLAYER_EJECTA,
player_cx + o_gun1[playerpic] - 4, player_cy - 4);
cur = SHOTS_Get();
if (!cur)
@@ -651,7 +654,8 @@ SHOTS_PlayerShoot(
cur->move.y2 = 0;
cur->startx = player_cx;
cur->starty = player_cy;
ANIMS_StartAnim(A_PLAYER_SHOOT, -o_gun1[playerpic] - 1, 0);
ANIMS_StartGAnim(A_PLAYER_EJECTA,
player_cx - o_gun1[playerpic] - 1 - 4, player_cy - 4);
break;
case S_PLASMA_GUNS:
+254 -58
View File
@@ -1,5 +1,6 @@
#include <string.h>
#include <stdio.h>
#include "SDL.h"
#include "common.h"
#include "glbapi.h"
#include "gfxapi.h"
@@ -182,6 +183,178 @@ WIN_OptDraw(
GFX_PutSprite((char*)GLB_GetItem(FILE127_SLIDE_PIC), x + opt_vol[FX_VOL] - 2, y);
}
// WIN_Opts value rows: field + selection icon, keyboard order (matches the
// dialog, top to bottom). The last two are the volume slider rows.
#define OPT_ROWS 8
#define OPT_VALUE_ROWS 6
#define FILTER_COUNT 4
static const int opt_filter_list[FILTER_COUNT] = { 0, 2, 3, 4 };
static const struct
{
int field;
int icon;
} opt_rows[OPT_ROWS] = {
{OPTS_DETAIL, OPTS_PIC1},
{OPTS_FILTER, OPTS_PIC4},
{OPTS_SCANLINES, OPTS_PIC5},
{OPTS_RES, OPTS_PIC6},
{OPTS_FULLSCR, OPTS_PIC7},
{OPTS_ASPECT, OPTS_PIC8},
{OPTS_VMUSIC, OPTS_PIC2},
{OPTS_VFX, OPTS_PIC3}
};
static const struct
{
int w, h;
} res_list[] = {
{640, 480}, {800, 600}, {960, 720}, {1280, 960},
{1600, 1200}, {1920, 1440}
};
#define RES_COUNT ((int)(sizeof(res_list) / sizeof(res_list[0])))
static const char opt_detail_txt[2][16] = {
"LOW DETAIL",
"HIGH DETAIL"
};
static int opt_curd;
static int opt_filter_idx;
static int opt_scan;
static int opt_res_idx;
static int opt_avail[RES_COUNT];
static int opt_fullscr;
static int opt_aspect;
static const char *OPTS_FilterText(char *buf)
{
if (opt_filter_list[opt_filter_idx] == 0)
strcpy(buf, "FILTER OFF");
else
sprintf(buf, "FILTER SCALE %dX", opt_filter_list[opt_filter_idx]);
return buf;
}
static const char *OPTS_ScanText(char *buf)
{
strcpy(buf, opt_scan ? "SCANLINES ON" : "SCANLINES OFF");
return buf;
}
static const char *OPTS_ResText(char *buf)
{
sprintf(buf, "RESOLUTION %dX%d", res_list[opt_res_idx].w,
res_list[opt_res_idx].h);
return buf;
}
static const char *OPTS_FullscrText(char *buf)
{
strcpy(buf, opt_fullscr ? "FULLSCREEN ON" : "FULLSCREEN OFF");
return buf;
}
static const char *OPTS_AspectText(char *buf)
{
strcpy(buf, opt_aspect ? "ASPECT 4:3 ON" : "ASPECT 4:3 OFF");
return buf;
}
/***************************************************************************
OPTS_ShowRow() - Moves the selection arrow icon to a row
***************************************************************************/
static void
OPTS_ShowRow(
int row
)
{
int loop;
for (loop = 0; loop < OPT_ROWS; loop++)
SWD_SetFieldItem(opt_window, opt_rows[loop].icon, -1);
SWD_SetFieldItem(opt_window, opt_rows[row].icon, FILE128_POINT_PIC);
SWD_ShowAllWindows();
GFX_DisplayUpdate();
}
/***************************************************************************
OPTS_StepRow() - Apply one LEFT(-1)/RIGHT(+1) step to an options row:
updates the row state, live-applies the video setting and refreshes the
field text. Returns 1 when the caller should play the slider patch sound.
***************************************************************************/
static int
OPTS_StepRow(
int field,
int dir
)
{
char buf[24];
switch (field)
{
case OPTS_VMUSIC:
opt_vol[MUSIC_VOL] += dir * 8;
if (opt_vol[MUSIC_VOL] < 0)
opt_vol[MUSIC_VOL] = 0;
if (opt_vol[MUSIC_VOL] > 127)
opt_vol[MUSIC_VOL] = 127;
return 0;
case OPTS_VFX:
opt_vol[FX_VOL] += dir * 8;
if (opt_vol[FX_VOL] < 0)
opt_vol[FX_VOL] = 0;
if (opt_vol[FX_VOL] > 127)
opt_vol[FX_VOL] = 127;
return 1;
case OPTS_DETAIL:
opt_curd ^= 1;
SWD_SetFieldText(opt_window, OPTS_DETAIL, opt_detail_txt[opt_curd]);
return 0;
case OPTS_FILTER:
opt_filter_idx = (opt_filter_idx + dir + FILTER_COUNT) % FILTER_COUNT;
I_SetVideoFilter(opt_filter_list[opt_filter_idx]);
SWD_SetFieldText(opt_window, OPTS_FILTER, OPTS_FilterText(buf));
return 0;
case OPTS_SCANLINES:
opt_scan ^= 1;
I_SetScanlines(opt_scan);
SWD_SetFieldText(opt_window, OPTS_SCANLINES, OPTS_ScanText(buf));
return 0;
case OPTS_RES:
do
{
opt_res_idx = (opt_res_idx + dir + RES_COUNT) % RES_COUNT;
} while (!opt_avail[opt_res_idx]);
I_SetVideoResolution(res_list[opt_res_idx].w, res_list[opt_res_idx].h);
SWD_SetFieldText(opt_window, OPTS_RES, OPTS_ResText(buf));
return 0;
case OPTS_FULLSCR:
opt_fullscr ^= 1;
I_SetFullscreen(opt_fullscr);
SWD_SetFieldText(opt_window, OPTS_FULLSCR, OPTS_FullscrText(buf));
return 0;
case OPTS_ASPECT:
opt_aspect ^= 1;
I_SetAspectCorrect(opt_aspect);
SWD_SetFieldText(opt_window, OPTS_ASPECT, OPTS_AspectText(buf));
return 0;
}
return 0;
}
/***************************************************************************
WIN_Opts() - Sets Game Options
***************************************************************************/
@@ -192,35 +365,79 @@ WIN_Opts(
{
SWD_DLG dlg;
int x, y, lx, ly;
int kbactive, patchflag, curd, cur_field;
int kbactive, patchflag, cur_field;
int new_vol;
int fpics[3] = {
OPTS_PIC1, OPTS_PIC2, OPTS_PIC3
};
char detail[2][16] = {
"LOW DETAIL",
"HIGH DETAIL"
};
int loop, diff, best;
SDL_DisplayMode desktop;
char buf[24];
kbactive = 0;
patchflag = 0;
curd = opt_detail;
cur_field = 0;
opt_vol[MUSIC_VOL] = music_volume;
opt_vol[FX_VOL] = fx_volume;
// Video row state from the live settings
opt_curd = opt_detail;
opt_filter_idx = 0;
for (loop = 0; loop < FILTER_COUNT; loop++)
{
if (opt_filter_list[loop] == I_GetVideoFilter())
opt_filter_idx = loop;
}
opt_scan = I_GetScanlines();
opt_fullscr = fullscreen;
opt_aspect = aspect_ratio_correct;
// Resolution presets: skip anything larger than the desktop, start on
// the preset closest in area to the current window size
if (SDL_GetDesktopDisplayMode(0, &desktop) != 0)
desktop.w = desktop.h = 0;
best = 0x7fffffff;
opt_res_idx = 0;
for (loop = 0; loop < RES_COUNT; loop++)
{
opt_avail[loop] = 1;
if (desktop.w && (res_list[loop].w > desktop.w
|| res_list[loop].h > desktop.h))
{
opt_avail[loop] = 0;
continue;
}
diff = res_list[loop].w * res_list[loop].h
- window_width * window_height;
if (diff < 0)
diff = -diff;
if (diff < best)
{
best = diff;
opt_res_idx = loop;
}
}
opt_window = SWD_InitWindow(FILE13f_OPTS_SWD);
SWD_SetFieldText(opt_window, OPTS_DETAIL, detail[curd]);
SWD_SetFieldText(opt_window, OPTS_DETAIL, opt_detail_txt[opt_curd]);
SWD_SetFieldText(opt_window, OPTS_FILTER, OPTS_FilterText(buf));
SWD_SetFieldText(opt_window, OPTS_SCANLINES, OPTS_ScanText(buf));
SWD_SetFieldText(opt_window, OPTS_RES, OPTS_ResText(buf));
SWD_SetFieldText(opt_window, OPTS_FULLSCR, OPTS_FullscrText(buf));
SWD_SetFieldText(opt_window, OPTS_ASPECT, OPTS_AspectText(buf));
SWD_SetWinDrawFunc(opt_window, WIN_OptDraw);
SWD_SetFieldItem(opt_window, OPTS_PIC1, -1);
SWD_SetFieldItem(opt_window, OPTS_PIC2, -1);
SWD_SetFieldItem(opt_window, OPTS_PIC3, -1);
SWD_ShowAllWindows();
OPTS_ShowRow(0);
SND_Patch(FX_SWEP, 127);
GFX_DisplayUpdate();
@@ -284,29 +501,11 @@ WIN_Opts(
break;
case SC_LEFT:
if (cur_field)
{
if (cur_field == 2)
patchflag = 1;
opt_vol[cur_field - 1] -= 8;
if (opt_vol[cur_field - 1] < 0)
opt_vol[cur_field - 1] = 0;
SWD_ShowAllWindows();
GFX_DisplayUpdate();
}
break;
case SC_RIGHT:
if (cur_field)
{
if (cur_field == 2)
patchflag = 1;
opt_vol[cur_field - 1] += 8;
if (opt_vol[cur_field - 1] > 127)
opt_vol[cur_field - 1] = 127;
SWD_ShowAllWindows();
GFX_DisplayUpdate();
}
patchflag = OPTS_StepRow(opt_rows[cur_field].field,
dlg.keypress == SC_RIGHT ? 1 : -1);
SWD_ShowAllWindows();
GFX_DisplayUpdate();
break;
case SC_UP:
@@ -314,33 +513,23 @@ WIN_Opts(
cur_field--;
kbactive = 1;
SND_Patch(FX_SWEP, 127);
SWD_SetFieldItem(opt_window, OPTS_PIC1, -1);
SWD_SetFieldItem(opt_window, OPTS_PIC2, -1);
SWD_SetFieldItem(opt_window, OPTS_PIC3, -1);
SWD_SetFieldItem(opt_window, fpics[cur_field], FILE128_POINT_PIC);
SWD_ShowAllWindows();
GFX_DisplayUpdate();
OPTS_ShowRow(cur_field);
break;
case SC_DOWN:
if (kbactive && cur_field < 2)
if (kbactive && cur_field < OPT_ROWS - 1)
cur_field++;
kbactive = 1;
SND_Patch(FX_SWEP, 127);
SWD_SetFieldItem(opt_window, OPTS_PIC1, -1);
SWD_SetFieldItem(opt_window, OPTS_PIC2, -1);
SWD_SetFieldItem(opt_window, OPTS_PIC3, -1);
SWD_SetFieldItem(opt_window, fpics[cur_field], FILE128_POINT_PIC);
SWD_ShowAllWindows();
GFX_DisplayUpdate();
OPTS_ShowRow(cur_field);
break;
case SC_ENTER:
if (!cur_field)
if (cur_field < OPT_VALUE_ROWS)
{
dlg.cur_act = S_FLD_COMMAND;
dlg.cur_cmd = F_SELECT;
dlg.field = OPTS_DETAIL;
dlg.field = opt_rows[cur_field].field;
}
break;
}
@@ -387,7 +576,7 @@ WIN_Opts(
}
}
opt_detail = curd;
opt_detail = opt_curd;
if (fx_volume != opt_vol[FX_VOL])
fx_volume = opt_vol[FX_VOL];
@@ -414,13 +603,22 @@ WIN_Opts(
INI_PutPreferenceLong("SoundFX", "Volume", fx_volume);
}
INI_PutPreferenceLong("Setup", "Detail", opt_detail);
INI_PutPreferenceLong("Video", "upscaler", I_GetVideoFilter());
INI_PutPreferenceLong("Video", "scanlines", I_GetScanlines());
INI_PutPreferenceLong("Video", "fullscreen", fullscreen);
INI_PutPreferenceLong("Video", "aspect_ratio_correct",
aspect_ratio_correct);
SND_Patch(FX_SWEP, 127);
goto exit_opts;
case OPTS_DETAIL:
case OPTS_FILTER:
case OPTS_SCANLINES:
case OPTS_RES:
case OPTS_FULLSCR:
case OPTS_ASPECT:
SND_Patch(FX_SWEP, 127);
curd ^= 1;
SWD_SetFieldText(opt_window, OPTS_DETAIL, detail[curd]);
OPTS_StepRow(dlg.field, 1);
SWD_ShowAllWindows();
GFX_DisplayUpdate();
break;
@@ -431,8 +629,6 @@ WIN_Opts(
SND_Patch(FX_SWEP, 127);
}
exit_opts:
SWD_SetWinDrawFunc(opt_window, NULL);
SWD_DestroyWindow(opt_window);
+1
View File
@@ -25,6 +25,7 @@ void WIN_AskExit(void);
void WIN_MainMenu(void);
void WIN_MainLoop(void);
void WIN_Pause(void);
void WIN_Opts(void);
int WIN_AskBool(const char *question);
void WIN_WinGame(int game);
void WIN_Msg(const char *msg);
+10
View File
@@ -194,3 +194,13 @@
#define OPTS_000A 0x000a // TEXT FIELD
#define OPTS_VMUSIC 0x000b // VIEW AREA
#define OPTS_VFX 0x000c // VIEW AREA
#define OPTS_FILTER 0x000d // BUTTON FIELD
#define OPTS_SCANLINES 0x000e // BUTTON FIELD
#define OPTS_RES 0x000f // BUTTON FIELD
#define OPTS_FULLSCR 0x0010 // BUTTON FIELD
#define OPTS_ASPECT 0x0011 // BUTTON FIELD
#define OPTS_PIC4 0x0012 // ICON
#define OPTS_PIC5 0x0013 // ICON
#define OPTS_PIC6 0x0014 // ICON
#define OPTS_PIC7 0x0015 // ICON
#define OPTS_PIC8 0x0016 // ICON
+104
View File
@@ -0,0 +1,104 @@
"""Spec for the re-authored data/ui/OPTS_SWD (catalog handle 0x1003f).
Same window/dialog as the original, grown from 13 to 23 fields: the original
Music/FX/Detail rows plus FILTER, SCANLINES, RESOLUTION, FULLSCREEN and
ASPECT rows and their selection icons. Window-relative coordinates assume
the window at x=60, y=6, 200x188 (320x200 screen space).
Layout (window-relative):
y=8 OPTIONS title (field 8)
y=20 panel bumpin behind rows (field 2, 5,20 190x100)
y=24 DETAIL row + PIC1 (fields 6, 3)
y=40 FILTER row + PIC4 (13, 18)
y=56 SCANLINES row + PIC5 (14, 19)
y=72 RES row + PIC6 (15, 20)
y=88 FULLSCR row + PIC7 (16, 21)
y=104 ASPECT row + PIC8 (17, 22)
y=122 MUSIC VOLUME label (9)
y=131 music slider view/track (11, 0)
y=146 SOUND FX VOLUME label (10)
y=160 fx slider view/track (12, 1)
y=174 EXIT button (7)
Build: python3 tools/swdbuild.py build tools/opts_swd_spec.py data/ui/OPTS_SWD
(the printed size must be written into data/INDEX's `1003f` line)
"""
from swdbuild import (BUMPIN_TEMPLATE, ICON_TEMPLATE, BUTTON_TEMPLATE,
TEXT_TEMPLATE, VIEWAREA_TEMPLATE)
SWIN = {
"version": 1, "swdsize": 0, "arrowflag": 0, "display": 0,
"opt3": 0, "opt4": 0, "id": 2, "type": 0,
"name": "OPTS", "item_name": "TEXTURE_PIC",
"item": 65559, "picflag": 1, "lock": 1, "fldofs": 120, "txtofs": 0,
"firstfld": 0, "opt": 1, "color": 1, "numflds": 23,
"x": 60, "y": 6, "lx": 200, "ly": 188, "shadow": 1,
# swdsize/txtofs are recomputed by the builder (file size / blob offset).
}
# All rows share the button look; value texts are overwritten at runtime via
# SWD_SetFieldText, so the stored strings are just the initial/label values.
_ROW = {**BUTTON_TEMPLATE, "x": 30, "lx": 140, "maxchars": 22}
_ICON = {**ICON_TEMPLATE, "x": 12, "y": 24}
_LABEL = {**TEXT_TEMPLATE, "x": 30}
FIELDS = [
# 0-2: bumpins (slider tracks + row panel), repositioned originals
{**BUMPIN_TEMPLATE, "x": 30, "y": 136, "lx": 127, "ly": 3},
{**BUMPIN_TEMPLATE, "x": 30, "y": 165, "lx": 127, "ly": 3},
{**BUMPIN_TEMPLATE, "x": 5, "y": 20, "lx": 190, "ly": 100},
# 3-5: selection icons for detail / music / fx rows
{**_ICON, "name": "PIC1", "y": 24},
{**_ICON, "name": "PIC2", "y": 122},
{**_ICON, "name": "PIC3", "y": 146},
# 6-7: buttons
{**_ROW, "name": "DETAIL", "hotkey": 0x20, "y": 24, "text_index": 2},
{**_ROW, "name": "EXIT", "id": 4, "hotkey": 0, "x": 118, "y": 174,
"lx": 57, "maxchars": 5, "text_index": 3},
# 8-10: texts
{**TEXT_TEMPLATE, "id": 200, "input_opt": 1, "item_name": "",
"font_name": "FONT1_FNT", "fontid": 0x10003, "x": 68, "y": 8,
"lx": 71, "ly": 14, "maxchars": 8, "text_index": 4},
{**_LABEL, "id": 150, "y": 122, "lx": 104, "maxchars": 13,
"text_index": 5},
{**_LABEL, "id": 160, "input_opt": 1, "y": 146, "lx": 112,
"maxchars": 16, "text_index": 6},
# 11-12: volume slider view areas
{**VIEWAREA_TEMPLATE, "name": "VMUSIC", "x": 30, "y": 131},
{**VIEWAREA_TEMPLATE, "name": "VFX", "x": 30, "y": 160},
# 13-17: video option rows (hotkeys: F, L, R, -, A)
{**_ROW, "name": "FILTER", "hotkey": 0x21, "y": 40, "text_index": 7},
{**_ROW, "name": "SCANLINES", "hotkey": 0x26, "y": 56, "text_index": 8},
{**_ROW, "name": "RES", "hotkey": 0x13, "y": 72, "text_index": 9},
{**_ROW, "name": "FULLSCR", "hotkey": 0, "y": 88, "text_index": 10},
{**_ROW, "name": "ASPECT", "hotkey": 0x1E, "y": 104, "text_index": 11},
# 18-22: selection icons for the five video rows
{**_ICON, "name": "PIC4", "y": 40},
{**_ICON, "name": "PIC5", "y": 56},
{**_ICON, "name": "PIC6", "y": 72},
{**_ICON, "name": "PIC7", "y": 88},
{**_ICON, "name": "PIC8", "y": 104},
]
# 0-1 ("Y","N") are the original bumpin marks; nothing references them at
# runtime but they are kept for parity with the extracted asset.
TEXTS = [
"Y", "N",
"DETAIL HIGH ", # 2: overwritten at open by WIN_Opts
"EXIT", # 3
"OPTIONS", # 4
"MUSIC VOLUME", # 5
"SOUND FX VOLUME", # 6
"FILTER SCALE 3X", # 7: overwritten at open
"SCANLINES OFF", # 8: overwritten at open
"RESOLUTION 960X720", # 9: overwritten at open
"FULLSCREEN OFF", # 10: overwritten at open
"ASPECT 4:3 ON", # 11: overwritten at open
]
+258
View File
@@ -0,0 +1,258 @@
#!/usr/bin/env python3
"""Dump and build Raptor .SWD dialog assets.
On-disk format (little-endian), mirrored from src/swdapi.h:
SWIN header 120 bytes <8i 16s 16s 14i>
SFIELD32[] 148 bytes <8i 16s 16s i 16s 15i i> (sptr -> PlaceHolder)
text blob NUL-padded strings
Each TEXT/BUTTON/INPUT/DRAGBAR field stores its text at `txtoff` RELATIVE
TO ITS OWN RECORD (SWD_ReformatFieldData compensates the 64-bit struct
growth with `(numflds - i) * 4`, which only closes under that reading).
Other field types never dereference txtoff.
Modes:
dump <file.swd> pretty-print SWIN, fields, text blob
build <spec.py> <out.swd> assemble a .swd from a Python spec
Spec module contract (see tools/opts_swd_spec.py):
from swdbuild import BUTTON_TEMPLATE, ... # clone-source records
SWIN = {...} # all 24 ints + name/item_name strings
FIELDS = [ {...}, ... ] # {**TEMPLATE, **overrides}; "text_index" links
# the field to TEXTS[i] and computes txtoff
TEXTS = ["...", ...] # blob entries, laid out in list order
"""
import argparse
import importlib.util
import struct
import sys
from pathlib import Path
SWIN_STRUCT = struct.Struct("<8i16s16s14i")
FIELD_STRUCT = struct.Struct("<8i16s16si16s15ii")
SWIN_KEYS = [
"version", "swdsize", "arrowflag", "display", "opt3", "opt4", "id", "type",
"name", "item_name",
"item", "picflag", "lock", "fldofs", "txtofs", "firstfld", "opt", "color",
"numflds", "x", "y", "lx", "ly", "shadow",
]
FIELD_KEYS = [
"opt", "id", "hotkey", "kbflag", "opt3", "opt4", "input_opt", "bstatus",
"name", "item_name", "item", "font_name",
"fontid", "fontbasecolor", "maxchars", "picflag", "color", "lite", "mark",
"saveflag", "shadow", "selectable", "x", "y", "lx", "ly",
]
FLD_TEXT, FLD_BUTTON, FLD_INPUT, FLD_DRAGBAR = 1, 2, 5, 6 # txtoff-carrying
def _s(raw: bytes) -> str:
return raw.split(b"\0", 1)[0].decode("ascii", "replace")
def _pack_str(text, field):
raw = text.encode("ascii")
if len(raw) > 16:
raise ValueError(f"{field}: string longer than 16 chars: {text!r}")
return raw.ljust(16, b"\0")
def _check_keys(where, given, required, optional=()):
missing = [k for k in required if k not in given]
extra = [k for k in given if k not in required and k not in optional]
if missing or extra:
raise ValueError(f"{where}: missing {missing}, unknown {extra}")
BUMPIN_TEMPLATE = {
"opt": 7, "id": 0, "hotkey": 0, "kbflag": 10, "opt3": 0, "opt4": 0,
"input_opt": 0, "bstatus": 0,
"name": "", "item_name": "",
"item": 0, "font_name": "SYSTEM_FNT", "fontid": 5, "fontbasecolor": 0,
"maxchars": 3, "picflag": 0, "color": 1, "lite": 15, "mark": 0,
"saveflag": 0, "shadow": 0, "selectable": 0,
"x": 0, "y": 0, "lx": 127, "ly": 3,
}
ICON_TEMPLATE = {
"opt": 9, "id": 0, "hotkey": 0, "kbflag": 30, "opt3": 0, "opt4": 0,
"input_opt": 0, "bstatus": 0,
"name": "PIC1", "item_name": "",
"item": -1, "font_name": "SYSTEM_FNT", "fontid": 5, "fontbasecolor": 0,
"maxchars": 1, "picflag": 2, "color": 8, "lite": 15, "mark": 0,
"saveflag": 0, "shadow": 0, "selectable": 0,
"x": 11, "y": 35, "lx": 16, "ly": 11,
}
BUTTON_TEMPLATE = {
"opt": 2, "id": 1, "hotkey": 0x20, "kbflag": 80, "opt3": 0, "opt4": 0,
"input_opt": 0, "bstatus": 0,
"name": "DETAIL", "item_name": "STEXTURE_PIC",
"item": 0x10024, "font_name": "FONT2_FNT", "fontid": 0x10004,
"fontbasecolor": 82, "maxchars": 13, "picflag": 1, "color": 8, "lite": 15,
"mark": 0, "saveflag": 0, "shadow": 1, "selectable": 0,
"x": 35, "y": 35, "lx": 118, "ly": 12,
}
TEXT_TEMPLATE = {
"opt": 1, "id": 150, "hotkey": 0, "kbflag": 90, "opt3": 0, "opt4": 0,
"input_opt": 0, "bstatus": 0,
"name": "", "item_name": "",
"item": 0, "font_name": "FONT2_FNT", "fontid": 0x10004, "fontbasecolor": 82,
"maxchars": 13, "picflag": 0, "color": 8, "lite": 15, "mark": 0,
"saveflag": 0, "shadow": 0, "selectable": 0,
"x": 35, "y": 57, "lx": 104, "ly": 8,
}
VIEWAREA_TEMPLATE = {
"opt": 11, "id": 0, "hotkey": 0, "kbflag": 110, "opt3": 0, "opt4": 0,
"input_opt": 0, "bstatus": 0,
"name": "VMUSIC", "item_name": "",
"item": 0, "font_name": "SYSTEM_FNT", "fontid": 5, "fontbasecolor": 0,
"maxchars": 1, "picflag": 0, "color": 8, "lite": 15, "mark": 0,
"saveflag": 0, "shadow": 0, "selectable": 0,
"x": 35, "y": 68, "lx": 127, "ly": 13,
}
def parse(path: Path):
"""Return (swin dict, field dicts, blob bytes) for a .swd file."""
data = path.read_bytes()
if len(data) < SWIN_STRUCT.size:
raise ValueError(f"{path}: {len(data)} bytes, shorter than SWIN header")
raw = SWIN_STRUCT.unpack_from(data, 0)
swin = dict(zip(SWIN_KEYS[:8], raw[:8]))
swin["name"] = _s(raw[8])
swin["item_name"] = _s(raw[9])
swin.update(dict(zip(SWIN_KEYS[10:], raw[10:])))
numflds = swin["numflds"]
fields = []
for i in range(numflds):
off = swin["fldofs"] + i * FIELD_STRUCT.size
if off + FIELD_STRUCT.size > len(data):
raise ValueError(f"{path}: field {i} at {off} past EOF")
fr = FIELD_STRUCT.unpack_from(data, off)
fld = dict(zip(FIELD_KEYS[:8], fr[:8]))
fld["name"] = _s(fr[8])
fld["item_name"] = _s(fr[9])
fld["item"] = fr[10]
fld["font_name"] = _s(fr[11])
fld.update(dict(zip(FIELD_KEYS[12:], fr[12:26])))
fld["txtoff"] = fr[26]
fld["_offset"] = off
fields.append(fld)
blob_start = swin["fldofs"] + numflds * FIELD_STRUCT.size
return swin, fields, data[blob_start:]
def dump(path: Path) -> None:
swin, fields, blob = parse(path)
print(f"file: {path} ({path.stat().st_size} bytes, blob at "
f"{SWIN_STRUCT.size + len(fields) * FIELD_STRUCT.size})")
print("SWIN:")
for k in SWIN_KEYS:
print(f" {k:<12} {swin[k]!r}")
for i, fld in enumerate(fields):
print(f"field[{i:2d}] at +{fld['_offset']}:")
for k in FIELD_KEYS:
print(f" {k:<14} {fld[k]!r}")
if fld["opt"] in (FLD_TEXT, FLD_BUTTON, FLD_INPUT, FLD_DRAGBAR):
blob_start = SWIN_STRUCT.size + len(fields) * FIELD_STRUCT.size
t = blob[fld["_offset"] + fld["txtoff"] - blob_start:]
text = t.split(b"\0", 1)[0].decode("ascii", "replace")
print(f" text {text!r}")
print()
def build(spec_path: Path, out_path: Path) -> None:
# Import swdbuild itself under its own name so specs can pull templates.
self_spec = importlib.util.spec_from_file_location(
"swdbuild", Path(__file__).resolve())
self_mod = importlib.util.module_from_spec(self_spec)
sys.modules["swdbuild"] = self_mod
self_spec.loader.exec_module(self_mod)
spec = importlib.util.spec_from_file_location("swd_spec", spec_path)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
swin_in, fields_in, texts = mod.SWIN, mod.FIELDS, mod.TEXTS
_check_keys("SWIN", swin_in, SWIN_KEYS)
if len(fields_in) != swin_in["numflds"]:
raise ValueError(f"SWIN.numflds={swin_in['numflds']} but "
f"len(FIELDS)={len(fields_in)}")
if swin_in["fldofs"] != SWIN_STRUCT.size:
raise ValueError(f"SWIN.fldofs must be {SWIN_STRUCT.size}")
# Lay out the text blob: entry i is padded to the max slot size its
# referencing fields need (SWD_SetFieldText writes maxchars bytes).
slot_size = [len(t.encode("ascii")) + 1 for t in texts]
for i, fld in enumerate(fields_in):
_check_keys(f"FIELDS[{i}] (after template merge)", fld,
FIELD_KEYS, optional=("text_index",))
ti = fld.get("text_index")
if ti is not None:
if not 0 <= ti < len(texts):
raise ValueError(f"FIELDS[{i}]: text_index {ti} out of range")
slot_size[ti] = max(slot_size[ti], fld["maxchars"])
blob = bytearray()
slot_off = []
for text, size in zip(texts, slot_size):
raw = text.encode("ascii")
if len(raw) >= size:
raise ValueError(f"TEXTS entry {text!r} does not fit slot {size}")
slot_off.append(len(blob))
blob += raw.ljust(size, b"\0")
blob_start = SWIN_STRUCT.size + len(fields_in) * FIELD_STRUCT.size
# Header bookkeeping: total file size and blob offset (the original
# asset stores exactly these; specs pass 0 and get the real values).
swin = dict(swin_in)
swin["swdsize"] = blob_start + len(blob)
swin["txtofs"] = blob_start
out = bytearray()
swin_vals = [swin[k] for k in SWIN_KEYS[:8]]
out += SWIN_STRUCT.pack(*swin_vals,
_pack_str(swin["name"], "SWIN.name"),
_pack_str(swin["item_name"], "SWIN.item_name"),
*[swin[k] for k in SWIN_KEYS[10:]])
for i, fld in enumerate(fields_in):
rec_off = SWIN_STRUCT.size + i * FIELD_STRUCT.size
txtoff = 0
ti = fld.get("text_index")
if ti is not None:
txtoff = blob_start + slot_off[ti] - rec_off
out += FIELD_STRUCT.pack(
*[fld[k] for k in FIELD_KEYS[:8]],
_pack_str(fld["name"], f"FIELDS[{i}].name"),
_pack_str(fld["item_name"], f"FIELDS[{i}].item_name"),
fld["item"],
_pack_str(fld["font_name"], f"FIELDS[{i}].font_name"),
*[fld[k] for k in FIELD_KEYS[12:]],
txtoff, 0)
assert len(out) == blob_start
out += blob
out_path.write_bytes(out)
print(f"{out_path}: {len(out)} bytes "
f"({len(fields_in)} fields, {len(texts)} text slots)")
def main() -> None:
ap = argparse.ArgumentParser(description=__doc__)
sub = ap.add_subparsers(dest="mode", required=True)
sub.add_parser("dump").add_argument("swd", type=Path)
bp = sub.add_parser("build")
bp.add_argument("spec", type=Path)
bp.add_argument("out", type=Path)
args = ap.parse_args()
if args.mode == "dump":
dump(args.swd)
else:
build(args.spec, args.out)
if __name__ == "__main__":
main()