Preserve current legacy presentation and options improvements
This commit is contained in:
1 parent
214789fe87
commit
eee082f2ad
15 files changed
+1090
-73
No files matched your search
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
@@ -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())
|
||||
{
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
]
|
||||
Executable
+258
@@ -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()
|
||||
Reference in new issue
Block a user