Compare commits

...
114 Commits
Author SHA1 Message Date
kichikuou 48458ac034 Statically link SDL3 on Windows and drop DLL collection 2026-09-23 08:04:59 +09:00
kichikuou ffdd362f35 Limit SDL2 SysWM header to Windows users 2026-09-23 07:24:01 +09:00
kichikuou 78519bb5ef CI: Use emscripten 6.0.10 2026-09-23 07:14:32 +09:00
kichikuou 5989fe72e2 Fetch SDL3_mixer when the package is unavailable 2026-09-23 07:14:15 +09:00
kichikuou 66d0636197 Port Windows menu integration to SDL3 2026-09-23 07:05:14 +09:00
kichikuou 6c8590f8d4 SDL: Fix screenshot and input compatibility 2026-09-23 06:46:02 +09:00
kichikuou 3cbe4c73d8 CI: Add SDL2 and SDL3 build matrix
Document the supported SDL configurations and build dependencies for each
platform. Keep Android on SDL3 only and identify every packaged artifact by
its SDL version.
2026-09-22 21:39:05 +09:00
kichikuou 80da6cd782 Android: Use SDL3_mixer AAR
Use the official SDL3_mixer Android archive through Prefab instead of
building SDL3_mixer from source. Verify both release archives and their
extracted AAR files with fixed SHA-256 hashes.
2026-09-22 21:27:40 +09:00
kichikuou 2f73abd486 Android: Migrate to SDL3
Use the official SDL 3.4.16 AAR through Prefab and build SDL_mixer
3.2.4 from fixed source. Remove the bundled SDL2 Java sources and make
Android an SDL3-only target.
2026-09-22 21:10:36 +09:00
kichikuou 5844226b2f Emscripten: Fix SDL3 compilation
Include the standard I/O declarations used by system.c directly and handle
SDL3's pointer-array locale API in the built-in NLS fallback.
2026-09-22 20:53:24 +09:00
kichikuou 87596c58c5 SDL: Keep mixer conversions simple
Remove the helper layer and unit tests added solely for simple gain and
time expressions. Keep these conversions directly in their SDL3_mixer
backends instead of shaping production code around testability.
2026-09-22 20:39:28 +09:00
kichikuou 212a7bf138 SDL: Test mixer time and volume conversions
Share SDL3_mixer gain and 10-millisecond time conversions across the
PCM, BGM, CD, and MIDI backends. Add SDL3 unit coverage for volume
clamping, combined gain, time units, and tick counter wraparound.
2026-09-22 20:34:26 +09:00
kichikuou 652724a9ab SDL: Keep MIDI decoder selection automatic
Do not force the Timidity decoder based on TIMIDITY_CFG. Leave decoder
selection to SDL3_mixer unless SDL_SOUNDFONTS requires FluidSynth for
SDL2_mixer-compatible SoundFont handling.
2026-09-22 16:05:19 +09:00
kichikuou 658debdcf3 SDL: Add SDL3 MIDI backend
Play MIDI through the shared SDL3_mixer music track while preserving
the existing loop, pause, volume, position, and fade behavior. Support
FluidSynth SoundFonts and Timidity selection through their established
environment variables.
2026-09-22 16:00:21 +09:00
kichikuou ed4fbc1f19 SDL: Add SDL3 CD audio backend
Load playlist tracks through SDL3_mixer and play them on the shared
owner-aware music track. Preserve the CD-specific loop convention,
volume valancer, and playback position reporting.
2026-09-22 15:51:54 +09:00
kichikuou c1910382f8 SDL: Add SDL3 BGM backend
Add an owner-aware music track shared by the SDL3 BGM, CD, and MIDI
backends. Use it to preserve exclusive music playback while moving BGM
loading, looping, fades, and volume control to SDL3_mixer.
2026-09-22 15:47:24 +09:00
kichikuou 495d6146f3 SDL: Add SDL3 PCM backend
Create shared SDL3_mixer device ownership and map the 129 PCM slots to
MIX_Track objects. Decode left/right WAV data through the public decoder
API before combining channels.
2026-09-22 15:41:00 +09:00
kichikuou 6f01734215 SDL: Add event and input compatibility
Map SDL2 and SDL3 event names, keyboard fields, touch queries, joystick
device identifiers, message boxes, and text input APIs through the common
interface.  Keep the deferred touch deadline on the millisecond clock so
SDL3's nanosecond event timestamps are not mixed with SDL_GetTicks().
2026-09-22 15:33:51 +09:00
kichikuou b55574c162 SDL: Add cursor compatibility
Map system cursor shapes to their SDL2 and SDL3 names and hide the
cursor destruction API difference.  Also include the allocation
declarations required by the SDL3 debug build.
2026-09-22 15:27:28 +09:00
kichikuou 9f92186905 SDL: Add display mode compatibility
Hide the SDL2 display index and SDL3 display ID and return-value
differences behind the compatibility layer.
2026-09-22 15:24:07 +09:00
kichikuou d80d82912a Build: Fix debug compilation errors
Include the standard allocation declarations where needed and use the
parsed command argument in TPC and TPS trace messages.
2026-09-22 15:23:45 +09:00
kichikuou eeb4953213 SDL: Add IO stream compatibility
Unify SDL2 RWops and SDL3 IOStream operations for files, memory,
font resources, and WAV decoding. Keep stream close ownership explicit
and cover memory stream reads in both SDL configurations.
2026-09-22 15:16:24 +09:00
kichikuou 5abc9e8177 SDL: Preserve blit destination updates
Emulate SDL2's clipped destination rectangle output when using SDL3.
Cover source clipping, destination clipping, and empty blits in the
SDL compatibility tests.
2026-09-22 15:10:05 +09:00
kichikuou 5d7ec418b1 SDL: Add renderer compatibility layer
Convert SDL2 integer render rectangles to SDL3 floating-point rectangles
inside the compatibility layer. Preserve logical presentation, integer
scaling, HiDPI sizing, texture filtering, and vertex color semantics.
2026-09-22 15:03:02 +09:00
kichikuou e488123345 SDL: Add thread primitive compatibility
Wrap mutexes, condition variables, and atomic integers with common SDL2
and SDL3 types and semantics.  Normalize timed condition waits to report
whether the condition was signaled.
2026-09-22 14:47:34 +09:00
kichikuou 5eea90c9a8 SDL: Add surface compatibility layer
Route surface formats, palettes, creation, conversion, and destruction
through SDL2/SDL3 compatibility helpers.  Preserve indexed-surface palette
ownership and adapt renamed fill, stretch, blit, and rectangle APIs.
2026-09-22 14:42:55 +09:00
kichikuou 677ecb5a0f Build: Add SDL version selection
Add the compile-time SDL major version option and propagate it through
interface targets. Route SDL includes through C99 compatibility headers
while keeping SDL2 as the desktop and Emscripten default.
2026-09-22 14:21:09 +09:00
kichikuou 1918350b19 Update censor lists for Rance 1-4 2026-09-21 14:26:11 +09:00
kichikuou 9504ee7b65 Add censor lists for Rance 1-4
Contributed by @RottenBlock and Vivvers (x.com/VivversX).
2026-09-21 13:17:23 +09:00
kichikuou f033a507d3 ZZ2: Return platform name from SDL
Fixes #82.
2026-09-20 12:23:02 +09:00
kichikuou fbeb9a780e Version 2.20.0 2026-09-11 14:40:11 +09:00
kichikuou 8c69f53073 SACT: Use bold font weight 2026-09-11 14:09:04 +09:00
kichikuou cc96c85381 SACT: Honor message skip state
The following SACT functions now return immediately while message
skipping is active:

- DrawEffect
- DrawEffectAlphaMap
- MessageOutput
- MessageOutputEx
- QuakeSprite
- TimerWait
- Wait
- SoundWaitKey
- WaitKeyMessage
2026-09-11 11:27:44 +09:00
kichikuou a2618319d9 SACT: MusicPlay() uses BGI music loop count 2026-09-11 08:58:26 +09:00
kichikuou 30fd5768da Avoid updating internal mouse position for System 3.8/3.9 games
Do not update the internal mouse position when cursor warping is
unavailable / disabled. System 3.8 and later can disable mouse automatic
cursor movement from the menubar, so no games for these versions are
designed with the assumption that this feature is available.
2026-09-11 07:57:00 +09:00
kichikuou 15a769d83c SACT: Increase MAX_TIMER to 128
Rakuen Yuki uses timer 101--103.
2026-09-11 07:22:27 +09:00
kichikuou 253de6adc8 Remove enable_zb option
Always enable ZB font weight handling and remove the enable_zb CLI and
profile configuration.
2026-09-06 16:12:19 +09:00
kichikuou a0daa3261c Font: Reduce synthetic bold strength at small sizes
Use half-pixel outline emboldening for font sizes up to 22 pixels to
better match GDI's synthetic bold rendering of MS Gothic and MS Mincho.
Keep the existing full-pixel strength for larger sizes.
2026-09-06 15:00:52 +09:00
kichikuou 98e9d8bcfb Debugger: Add cache statistics command
Add `info cache` command that reports cache usage and cumulative hit
rates for glyph, CG, and archive data.
2026-09-06 14:02:03 +09:00
kichikuou b7854aaec1 Generalize the glyph cache
Replace the old cache manager with a keyed LRU cache and use it for
glyph, CG, and archive data.
2026-09-06 14:02:03 +09:00
kichikuou 905eb34590 Font: Add glyph cache
Cache rendered glyph bitmaps by font properties with a 4 MiB LRU limit.
2026-09-06 14:02:03 +09:00
kichikuou 8f22566adb Add unit test for font.c 2026-09-06 14:02:00 +09:00
kichikuou 48ded43e2a Fix menu text being erased when removing selection highlights
Restore the saved area for opaque menus as well as translucent ones.
Painting over highlights with the background color could erase glyph
pixels in the selected row and the next row.

Also match the original engine's menu item text position.
2026-09-06 13:48:07 +09:00
kichikuou 571bec63a2 Windows: Use msgothic.ttc / msmincho.ttc by default
Fall back to the bundled fonts when these fonts are unavailable.
2026-09-06 13:46:46 +09:00
kichikuou 345c017c22 Render text with FreeType instead of SDL_ttf
SDL_ttf's synthetic bold is too thick. Implement a custom bolding
algorithm that approximates GDI's synthetic bold.
2026-09-06 13:13:09 +09:00
kichikuou 205004086b Move drawing text to the screen out of font.c
This makes font.c unit-testable.
2026-09-06 13:10:20 +09:00
kichikuou 6820bcfcb8 Derive default patchEC value from scenario signature
The EC bug exists only in System 3.8 and later. System 3.5 / 3.6 games
(e.g. Atlach=Nacha) need the patched behavior.

This deviates from System39.exe, which has the buggy behavior for older
scenarios too.
2026-09-06 13:03:51 +09:00
kichikuou 5ce1494b59 Match the original engine's patchEC behavior
The patchEC flag is default off, and EC 0 goes through the same
width/height adjustment as EC with a registered rectangle.
2026-09-05 12:23:09 +09:00
kichikuou dd45e566f6 Emscripten: Remove xsystem35.shell.quit() call in sys_exit()
sys_exit() is unreachable in emscripten build.
2026-08-31 16:37:15 +09:00
kichikuou 1ccc7630c1 QNT: Fix alpha decoding of 1-pixel-wide images 2026-08-31 07:02:44 +09:00
kichikuou 6456f59c37 emscripten: Add JSPI build option 2026-08-21 08:54:34 +09:00
reiicheru f748e8f678 android: Hide android status bar when app is resumed (#81) 2026-08-10 09:44:56 +09:00
kichikuou a9e23508b7 Reimplement the string input dialog with a microui textbox
The dialog edited and drew its field by hand because microui's textbox
cannot display IME composition text.

Teach microui about compositions instead, and split mu_textbox_raw()
into input and drawing halves so the MJ inline editor can share the
input path while keeping the game's own appearance.
2026-08-08 13:03:31 +09:00
kichikuou c3bd978f02 microui: Add keyboard navigation
ctx->focus is a transient "pressed with the mouse" state, so it cannot
serve as a keyboard focus. Add a persistent ctx->kb_focus alongside it.
Controls enter the focus cycle from mu_update_control(), which every
widget already calls, so this needs no per-widget code.

Tab and Down move the focus forward, Shift+Tab and Up backward, Enter or
Space activates the focused control, and Left/Right adjust a focused
slider. The focused control is outlined with the new MU_COLOR_FOCUSRING;
window chrome is excluded via MU_OPT_NOKBNAV.
2026-08-08 10:17:02 +09:00
kichikuou 50a6b17c5e Version 2.19.1 2026-08-02 12:41:18 +09:00
kichikuou 0d49619366 Fix unit map storage for 16-bit variables
Store unit map attributes as vmvar_t and derive VR/VW transfer sizes
from their element type. This prevents buffer overruns after vmvar_t
became uint16_t and fixes upward movement in Rance IV.

Add scenario coverage for both transfer directions and verify that
adjacent variables remain intact.
2026-08-02 12:34:40 +09:00
kichikuou da3cc5d900 ags: Consolidate display info accessors 2026-07-24 21:04:25 +09:00
kichikuou 1785e93fcc Refactor gfx API 2026-07-24 21:04:22 +09:00
kichikuou a77e849f82 Version 2.19.0 2026-07-19 12:53:09 +09:00
kichikuou 978dbdbc3b Tweak menu strings
Also, do not show "Mouse Movement" item on Android.
2026-07-19 12:46:03 +09:00
kichikuou e3c168c5a6 Android: Isolate game execution process
The launcher and GameActivity previously ran in the same process. Native
globals and library state therefore survived after returning to the
launcher, leaking static data from one game to the next game.

Run GameActivity in a private :game process and terminate it after SDL
shutdown completes, so every game starts with fresh native state. Keep
the launcher in the main process and refresh its game list from disk
when it resumes.
2026-07-19 09:09:21 +09:00
kichikuou 9f8c2af842 Emscripten: Fix ReferenceError for SE_VOLVAL_CH
EM_ASYNC_JS stringifies its function body without expanding macros. Pass
the BGM and sound-effect channel values through C wrapper functions so
the generated JavaScript receives numeric channel arguments.
2026-07-19 07:20:53 +09:00
kichikuou 5d8b1c5632 Change vmvar_t from int to uint16_t 2026-07-17 09:50:21 +09:00
kichikuou 30188c3a4f Match variable bounds with System 3.9
System 3.9 saturates compound addition and multiplication at 65535,
while an overflowing ascending loop stores the wrapped 16-bit result
after testing the wide value.

Update the scenario expectations and regenerated fixture to cover both
cases. The suite passes with xsystem35 and the original System39.exe.
2026-07-17 09:50:02 +09:00
kichikuou a627a8ea3d Add VM variable storage type
Define a dedicated `vmvar_t` type for values stored in variable pages
and propagate it through interfaces that expose those values. Keep the
type as int for now so this commit does not change runtime behavior.
This separates VM storage from ordinary int buffers and makes a later
conversion to 16-bit storage mechanically checkable.
2026-07-17 08:48:10 +09:00
kichikuou 0cf9c1fe8d Add variable semantics tests
Cover variable page allocation, array reference handling, and Shift-JIS
conversion in src_tests.

Extend the scenario suite with 16-bit boundary behavior for assignments,
loops, module results, and save/load round trips.
2026-07-17 08:11:01 +09:00
kichikuou 0887c44292 Run scenario tests headlessly
Register the existing scenario suite with CTest using SDL's dummy
video and audio drivers.
2026-07-16 20:53:19 +09:00
kichikuou 27a0c89a73 QNT: Add odd-size image test 2026-07-10 08:54:22 +09:00
kichikuou 5c827225ab QNT: Calculate decompression buffers exactly
Round image dimensions up to even values for QNT's 2x2 pixel blocks, and
use the resulting exact size for pixel and alpha decompression.

Verification against Only You Re Cross's QNT files confirmed that the
former 1164-byte discrepancy is odd-height padding, not extra zlib
output. Remove the obsolete fixed margin.
2026-07-10 08:54:19 +09:00
kichikuou bc37ba6256 Require SDL2 >=2.18.0 2026-07-09 09:50:44 +09:00
kichikuou ab4bc2d8d1 Move more modules into src_lib 2026-07-09 07:16:59 +09:00
kichikuou f26321c5e0 Android: Add some comments to CdImageReader 2026-07-05 16:34:16 +09:00
kichikuou 1d897c72d0 Android: Add CD image unit tests
Add synthetic ISO9660, CUE, CCD, and MDS fixtures for JVM unit tests.
Cover CD image sector mapping, audio extraction, install cleanup,
playlist generation, and output path validation.
2026-07-05 16:21:17 +09:00
kichikuou 172c76b4e2 Update android/README.md 2026-07-05 14:51:50 +09:00
kichikuou 100584f896 Android: Support CCD and MDS CD images
Extend the Android CD image reader to accept .bin/.img + .ccd and
.mdf/.mds pairs. CCD metadata is converted into the existing track
model, and MDS descriptors provide data and audio track offsets for
extraction.
2026-07-05 14:48:05 +09:00
kichikuou 975f618b53 Android: Support .img/.cue installs
Extend the Android CD image reader to parse .bin/.img + .cue images
and map MODE1 data tracks into the ISO9660 extractor. Audio tracks are
extracted as WAV files under GAMEDATA/cdda, and playlist.txt is
generated from the CUE track numbers.
2026-07-05 14:20:51 +09:00
kichikuou 0c8bab0c64 Android: Add ISO CD image installer
Add a CD image install path that accepts selected files from the
launcher and extracts GAMEDATA from ISO9660 images. The installer
reuses the existing install transaction cleanup and writes
game_directory.txt so installed games launch through the normal
GameActivity path.
2026-07-05 13:37:15 +09:00
kichikuou 0192fd3d8e Android: Split launcher storage and install logic
Move ZIP installation and file-store responsibilities out of Launcher.
Launcher now coordinates install state and delegates game scanning,
save-data operations, and ZIP extraction to focused helper classes.
2026-07-05 12:28:57 +09:00
kichikuou d69fc5c6a3 Android: Keep launcher install state in Launcher
Move ZIP install jobs from GlobalScope to a Launcher-owned coroutine
scope and store progress, success, and failure in InstallState. This
lets a recreated LauncherActivity render the current install state and
consume completed results once, even when observer callbacks happened
while the activity was absent.
2026-07-05 11:24:40 +09:00
kichikuou 68a39843e2 Android: Prevent ZIP path traversal in launcher 2026-07-05 10:16:48 +09:00
kichikuou e3d7b60785 Emscripten: Apply volume-valancer levels to the shell 2026-07-04 16:17:31 +09:00
kichikuou 2bb891ada8 Move WAI parser to audio_meta.c (renamed from bgi.c) 2026-07-04 16:17:28 +09:00
kichikuou f109fe11f5 Replace event_custom_handler with modal_handle_event 2026-07-01 17:57:14 +09:00
kichikuou 73b3377b77 Add option to mute audio while the window is unfocused
Disabled by default; enable via the mute_on_unfocus rc setting or the
-mute_on_unfocus flag.

Fixes #37.
2026-07-01 17:36:58 +09:00
kichikuou 9400572502 Rename menu_render_overlay to modal_render_overlay 2026-07-01 17:26:04 +09:00
kichikuou a1e063c4b2 Enable the volume controller dialog on Emscripten
Export volume_dialog_open() so the web shell can open it, and add
modal_run to ASYNCIFY_ADD since it drives an async loop.
2026-07-01 17:21:49 +09:00
kichikuou 602c55a5e5 Enable UI translation on platforms without libintl
Compile po/ja.po into a built-in catalog and select it by locale via
SDL_GetPreferredLocales(), so translations work without libintl.
2026-07-01 13:53:18 +09:00
kichikuou 9624192a41 Provide native input/menu implementations directly on Windows
Let win/dialog.c and win/menubar.c define input_modal_string,
input_modal_number, menu_open and menu_init directly instead of
dispatching to them through wrappers.
2026-07-01 10:53:14 +09:00
kichikuou 5c7436e2b1 Factor out the confirmation dialog in menu.c 2026-07-01 10:24:52 +09:00
kichikuou 6c9844697f Merge menu_emscripten.c into menu.c
Consolidate the SDL and Emscripten menu backends into a single menu.c.
2026-07-01 10:11:32 +09:00
kichikuou 210c0a13e4 Reword the quit/restart confirmation dialogs 2026-07-01 09:23:44 +09:00
kichikuou 3ebb1faece Use the SDL popup menu on Android
The popup menu opens with a three-finger tap. Add touch support to the
modal framework: a tap is replayed as a multi-frame mouse
hover->press->release so microui registers the click, and the press is
held so sliders can be dragged. Fix a microui command-buffer alignment
bug that crashed on 32-bit ARM.
2026-07-01 09:23:38 +09:00
kichikuou 286f150510 Android/Emscripten: Move input dialogs into input_modal.c
Consolidate the Android and Emscripten implementations of the string/
number input dialogs into input_modal.c, and split the input-dialog API
out of menu.h into input_modal.h.
2026-07-01 08:22:14 +09:00
kichikuou a194cf10a6 Reimplement the windowless string input on the modal framework
Move edit_string into input_modal.c as an inline modal dialog, sharing the
IME buffer handling and event handler with menu_inputstring.
2026-07-01 08:22:09 +09:00
kichikuou 9587d350ec Split string/number input dialogs out of menu_sdl.c into input_modal.c 2026-06-30 14:17:44 +09:00
kichikuou 02f62d51bb Add Restart to the SDL popup menu
Also localize the quit/restart confirmation dialogs.
2026-06-30 12:34:14 +09:00
kichikuou 54b98ec3e4 Translate UI strings and guard the .pot against staleness
Translate the menu/dialog strings that were left untranslated. Add a CI
step that fails when po/xsystem35.pot is out of sync with the sources.

Also remove unused autotools gettext files.
2026-06-30 12:20:51 +09:00
kichikuou 8cde827ee6 modal: Reset input state when a modal dialog closes
The click (or keypress) that opened a modal dialog leaves its button
"down" in the engine, and the modal consumes the matching release, so
afterwards IK kept reporting a stuck mouse button (or key). Forget all
held keys/buttons when a modal closes.
2026-06-30 10:04:19 +09:00
kichikuou a5d67b067c Use System39.exe-compatible format for Volume.sav
Also, save Volume.sav when the volume dialog is closed.
2026-06-29 17:18:49 +09:00
kichikuou 5cb70110b9 Apply BGM_VOLVAL_CH to MIDI and CD playback
MIDI and CD are background music, so they should share the same volume
control as BGM. Route them through the BGM volume valancer channel.
2026-06-29 16:17:13 +09:00
kichikuou e3dcadd5dd Allow the volume dialog to work without VolumeValancer
When a game defines no VolumeValancer[], fall back to two generic
channels so BGM and sound effects can be adjusted independently.

Volume controls are now always available, so drop the conditional menu
handling.
2026-06-29 15:52:08 +09:00
kichikuou ff31d03153 Implement mus_vol_set_valance()
Apply the System39.ini volume valancer levels to audio output, so the
Volume controller dialog actually adjusts per-channel volumes. PCM scales
its chunk volume by the channel mapped via the .wai file, and BGM scales
its music volume by valancer channel 0.

Fixes #62.
2026-06-29 11:44:22 +09:00
kichikuou 63886d7d00 Make the volume dialog's close button functional 2026-06-29 11:44:21 +09:00
kichikuou c119c2f969 Enable the volume controller dialog on Windows
Rename s39init.{c,h} to volume.{c,h}, move the dialog code there behind
volume_dialog_open(), and add a "Sound Settings" item to the Windows
menu.
2026-06-29 11:44:18 +09:00
kichikuou fb63d42ec1 Remove popupmenu_opened flag
With the blocking modal_run(), the game's main loop is frozen while a
menu is open, so the flag's input-suppression and VM-pause checks are
unreachable. Quit requested during a modal is now handled by closing the
modal and letting it propagate through the main loop.
2026-06-28 13:27:39 +09:00
kichikuou 6f631d073d menu_sdl: Use a light color scheme
Override microui's dark default palette with a light scheme (light gray
windows, dark text) in init_context().
2026-06-28 13:27:39 +09:00
kichikuou 49dca57fbd Call setlocale() so menu translations work
gettext only honors LANG / LC_* once setlocale() has been called. This
used to happen inside gtk_init(); now that GTK is gone, call
setlocale(LC_ALL, "") ourselves before bindtextdomain(), otherwise the
locale stays "C" and the menu is always shown in English.
2026-06-28 13:27:39 +09:00
kichikuou c8987da470 menu: Remove GTK menu backend
Linux and macOS now both build the pure-SDL + microui menu. Remove GTK3
detection, the ENABLE_GTK build branches, and the GTK menu sources.

Decouple gettext NLS from ENABLE_GTK so menu labels stay translatable on all
desktop builds, and update i18n to the new sources (POTFILES.in, .pot, ja.po).
2026-06-28 13:27:35 +09:00
kichikuou 0dfb3eb378 menu_sdl: Implement volume controller panel
Rewrite s39init.c GTK-free: keep the System39.ini / Volume.sav loading,
saving and mus_vol_set_valance() logic, and expose the channel table via
s39ini_available() / s39ini_channels() instead of driving a GtkWidget.

Add a microui volume panel to menu_sdl.c, offered as a "Volume" item in
the popup menu when the game defines volume channels.
2026-06-28 13:08:25 +09:00
kichikuou 269132fede menu_sdl: Implement the string-input dialog
The text field is hand-drawn and IME-aware: SDL_TEXTINPUT /
SDL_TEXTEDITING / BACKSPACE are handled directly (mirroring editor.c)
instead of using a microui textbox.
2026-06-28 13:07:42 +09:00
kichikuou 651fc21a19 menu_sdl: Implement the number-input dialog 2026-06-28 09:49:22 +09:00
kichikuou 03f86bc4d7 menu: Add pure SDL popup menu using microui
Vendor microui 2.02 under src/microui/ and use it to draw an in-game
popup menu as an overlay.

modal.c provides generic modal infrastructure: a loop that pumps SDL
events into microui and renders the command list via SDL_Renderer,
hooked into gfx_updateScreen(). menu_sdl.c builds the concrete popup
menu on top of it.
2026-06-28 09:49:19 +09:00
kichikuou 4ffab8e4e9 Fix compiler warning in cg.c 2026-06-27 16:18:09 +09:00
kichikuou 1593452146 menu: Signal number-input cancellation via the return value
It was inconsistent across backends. Now: true = confirmed,
false = cancelled.
2026-06-25 13:25:29 +09:00
261 changed files with 12047 additions and 10458 deletions
+1
View File
@@ -24,5 +24,6 @@ jobs:
- name: Upload artifacts
uses: actions/upload-artifact@v7
with:
name: xsystem35-android-sdl3
path: android/app/build/outputs/apk/release/app-release.apk
archive: false
+15 -9
View File
@@ -2,12 +2,17 @@ name: Emscripten Build
on: [push, pull_request]
env:
EM_VERSION: latest
EM_VERSION: 6.0.10
EM_CACHE_FOLDER: 'emsdk-cache'
jobs:
build:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
sdl-version: [2, 3]
name: Emscripten SDL${{ matrix.sdl-version }}
steps:
- uses: actions/checkout@v6
@@ -17,7 +22,7 @@ jobs:
uses: actions/cache@v5
with:
path: ${{env.EM_CACHE_FOLDER}}
key: ${{env.EM_VERSION}}-${{ runner.os }}
key: ${{env.EM_VERSION}}-${{ runner.os }}-sdl${{ matrix.sdl-version }}
- name: Setup Emscripten toolchain
uses: emscripten-core/setup-emsdk@v16
@@ -30,15 +35,16 @@ jobs:
- name: Build
run: |
mkdir -p out/wasm
cd out/wasm
emcmake cmake -DCMAKE_BUILD_TYPE=MinSizeRel -DCMAKE_COMPILE_WARNING_AS_ERROR=YES ../../
make -j4
mkdir xsystem35
mv src/xsystem35.* xsystem35/
emcmake cmake -S . -B out/wasm \
-DCMAKE_BUILD_TYPE=MinSizeRel \
-DCMAKE_COMPILE_WARNING_AS_ERROR=YES \
-DXSYSTEM35_SDL_VERSION=${{ matrix.sdl-version }}
cmake --build out/wasm -j4
mkdir out/wasm/xsystem35
mv out/wasm/src/xsystem35.* out/wasm/xsystem35/
- name: Upload artifacts
uses: actions/upload-artifact@v7
with:
name: xsystem35-wasm
name: xsystem35-wasm-sdl${{ matrix.sdl-version }}
path: out/wasm/xsystem35
+27 -9
View File
@@ -3,27 +3,45 @@ on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
runs-on: ubuntu-26.04
strategy:
fail-fast: false
matrix:
build-type: ["Debug", "Release"]
sdl-version: [2, 3]
name: Linux ${{ matrix.build-type }}
name: Linux SDL${{ matrix.sdl-version }} ${{ matrix.build-type }}
steps:
- uses: actions/checkout@v6
- name: Install Deps
run: |
sudo apt update
sudo apt install libgtk-3-dev libsdl2-dev libsdl2-ttf-dev libsdl2-mixer-dev libwebp-dev libportmidi-dev libcjson-dev asciidoctor
sudo apt install libfreetype-dev libwebp-dev libportmidi-dev libcjson-dev asciidoctor gettext
if [ "${{ matrix.sdl-version }}" = 2 ]; then
sudo apt install libsdl2-dev libsdl2-mixer-dev
else
sudo apt install libsdl3-dev
fi
- name: Build
run: |
mkdir -p out/${{ matrix.build-type }}
cd out/${{ matrix.build-type }}
cmake -DCMAKE_BUILD_TYPE=${{ matrix.build-type }} ../../
make -j4
cmake -S . -B out/build -DCMAKE_BUILD_TYPE=${{ matrix.build-type }} \
-DXSYSTEM35_SDL_VERSION=${{ matrix.sdl-version }}
cmake --build out/build -j4
- name: Test
run: ctest --output-on-failure
working-directory: out/${{ matrix.build-type }}
run: ctest --test-dir out/build --output-on-failure
- name: Verify xsystem35.pot is up to date
if: matrix.build-type == 'Debug' && matrix.sdl-version == 2
run: |
xgettext --default-domain=xsystem35 --directory=. \
--keyword=_ --keyword=N_ \
--files-from=po/POTFILES.in \
--output=/tmp/fresh.pot
if ! diff <(grep '^msgid' po/xsystem35.pot | sort) \
<(grep '^msgid' /tmp/fresh.pot | sort); then
echo "::error::po/xsystem35.pot is out of date. Build the 'pot' target and commit the changes under po/."
exit 1
fi
+22 -10
View File
@@ -9,11 +9,23 @@ jobs:
matrix:
include:
- sys: mingw32
package: "xsystem35-32bit"
package: "xsystem35-sdl2-32bit"
sdl-version: 2
sdl-deps: "SDL2:p SDL2_mixer:p"
cmake_args: "-DENABLE_DEBUGGER=no"
deps: ""
- sys: ucrt64
package: "xsystem35-64bit"
package: "xsystem35-sdl2-64bit"
sdl-version: 2
sdl-deps: "SDL2:p SDL2_mixer:p"
cmake_args: ""
deps: >-
cjson:p
portmidi:p
- sys: ucrt64
package: "xsystem35-sdl3-64bit"
sdl-version: 3
sdl-deps: "sdl3:p sdl3-mixer:p"
cmake_args: ""
deps: >-
cjson:p
@@ -21,7 +33,7 @@ jobs:
defaults:
run:
shell: msys2 {0}
name: MSYS2 ${{ matrix.sys }}
name: MSYS2 SDL${{ matrix.sdl-version }} ${{ matrix.sys }}
steps:
- name: Set up MSYS2
@@ -29,9 +41,8 @@ jobs:
with:
msystem: ${{ matrix.sys }}
pacboy: >-
SDL2:p
SDL2_ttf:p
SDL2_mixer:p
${{ matrix.sdl-deps }}
freetype:p
libwebp:p
${{ matrix.deps }}
@@ -50,10 +61,11 @@ jobs:
- name: Build
run: |
mkdir -p out/release
cd out/release
cmake -GNinja -DCMAKE_BUILD_TYPE=Release -DCMAKE_COMPILE_WARNING_AS_ERROR=YES ${{ matrix.cmake_args }} ../../
ninja
cmake -S . -B out/release -GNinja -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_COMPILE_WARNING_AS_ERROR=YES \
-DXSYSTEM35_SDL_VERSION=${{ matrix.sdl-version }} \
${{ matrix.cmake_args }}
cmake --build out/release
- name: Test
run: ctest --output-on-failure
+1
View File
@@ -1,6 +1,7 @@
out
android/.gradle
android/.idea
android/app/aars
android/app/.cxx
android/app/.externalNativeBuild
android/app/build
+24
View File
@@ -1,5 +1,29 @@
# Changelog
## 2.20.0 - 2026-09-11
- Improved font rendering to more closely match the original engine. On
Windows, MS Gothic and MS Mincho are now used by default when available.
- The experimental `enable_zb` option has been removed. (#44)
- SACT: Message skipping now also skips text animation, effects, waits, and
related input waits.
- SACT: BGM now honors the loop count specified by the game.
- SACT: Fixed timer handling in 楽園行.
- The popup menu and dialogs can now be operated with the keyboard.
- Fixed mouse coordinates changing unexpectedly when automatic mouse movement
is disabled in System 3.8/3.9 games.
- Android: The status bar now stays hidden after returning to the game. (#81)
- Debugger: Added an `info cache` command for viewing cache usage and hit rates.
## 2.19.1 - 2026-08-02
- Fixed an issue that prevented moving upward during battles in Rance 4.
## 2.19.0 - 2026-07-19
- Replaced the GTK-based popup menu and dialogs with a pure-SDL implementation.
- On Android, the popup menu opens with a three-finger tap.
- Android: Added installation directly from CD-ROM images.
- Implemented volume control panel.
- Added `mute_on_unfocus` option to mute audio while the window is unfocused.
## 2.18.0 - 2026-06-23
- Added support for the intro demo of Daiakuji.
- Replaced the bundled mincho font with IPA Mincho.
+88 -31
View File
@@ -5,7 +5,24 @@ set(CMAKE_C_STANDARD 99)
enable_testing()
set(CMAKE_EXPORT_COMPILE_COMMANDS TRUE)
set(XSYSTEM35_VERSION "2.18.0")
set(XSYSTEM35_VERSION "2.20.0")
if (ANDROID)
set(XSYSTEM35_DEFAULT_SDL_VERSION 3)
else()
set(XSYSTEM35_DEFAULT_SDL_VERSION 2)
endif()
set(XSYSTEM35_SDL_VERSION "${XSYSTEM35_DEFAULT_SDL_VERSION}" CACHE STRING
"SDL major version (2 or 3)")
set_property(CACHE XSYSTEM35_SDL_VERSION PROPERTY STRINGS 2 3)
if (NOT XSYSTEM35_SDL_VERSION STREQUAL "2" AND
NOT XSYSTEM35_SDL_VERSION STREQUAL "3")
message(FATAL_ERROR "XSYSTEM35_SDL_VERSION must be 2 or 3")
endif()
add_library(xsystem35_sdl INTERFACE)
target_compile_definitions(xsystem35_sdl INTERFACE
XSYSTEM35_SDL_VERSION=${XSYSTEM35_SDL_VERSION})
add_library(xsystem35_sdl_mixer INTERFACE)
include(CheckSymbolExists)
include(FetchContent)
@@ -55,6 +72,8 @@ check_symbol_exists(mmap "sys/mman.h" HAVE_MMAP)
check_symbol_exists(sigaction "signal.h" HAVE_SIGACTION)
if (EMSCRIPTEN)
option(JSPI "Use JavaScript Promise Integration instead of Asyncify" OFF)
function(add_emscripten_library name option)
add_library(${name} INTERFACE)
set_target_properties(${name} PROPERTIES
@@ -62,16 +81,23 @@ if (EMSCRIPTEN)
INTERFACE_LINK_OPTIONS ${option})
endfunction()
add_emscripten_library(zlib -sUSE_ZLIB=1)
add_emscripten_library(sdl2 -sUSE_SDL=2)
add_emscripten_library(sdl2_ttf -sUSE_SDL_TTF=2)
target_compile_options(xsystem35_sdl INTERFACE
-sUSE_SDL=${XSYSTEM35_SDL_VERSION})
target_link_options(xsystem35_sdl INTERFACE
-sUSE_SDL=${XSYSTEM35_SDL_VERSION})
add_emscripten_library(freetype2 -sUSE_FREETYPE=1)
set(DEFAULT_FONT_PATH /fonts/)
fetch_webp()
set(HAVE_WEBP 1)
elseif (ANDROID)
add_library(sdl2 ALIAS SDL2)
add_library(sdl2_ttf ALIAS SDL2_ttf)
add_library(sdl2_mixer ALIAS SDL2_mixer)
if (NOT XSYSTEM35_SDL_VERSION STREQUAL "3")
message(FATAL_ERROR "Android supports SDL3 only")
endif()
target_link_libraries(xsystem35_sdl INTERFACE SDL3::SDL3)
target_link_libraries(xsystem35_sdl_mixer INTERFACE
SDL3_mixer::SDL3_mixer)
add_library(freetype2 ALIAS freetype)
find_library(ndk_log log)
find_library(ndk_zlib z)
add_library(zlib INTERFACE)
@@ -87,25 +113,48 @@ else() # non-emscripten, non-android
add_library(zlib ALIAS ZLIB::ZLIB)
include(FindPkgConfig)
optional_pkg_check_modules(GTK3 IMPORTED_TARGET gtk+-3.0)
if (GTK3_FOUND)
set(ENABLE_GTK 1)
endif()
pkg_check_modules(SDL2 REQUIRED IMPORTED_TARGET sdl2)
pkg_check_modules(SDL2TTF REQUIRED IMPORTED_TARGET SDL2_ttf)
pkg_check_modules(SDL2MIXER REQUIRED IMPORTED_TARGET SDL2_mixer)
if (WIN32)
add_static_library(sdl2 SDL2)
add_static_library(sdl2_ttf SDL2TTF)
# Workaround for linking error
set_property(TARGET sdl2_ttf PROPERTY INTERFACE_LINK_LIBRARIES
$<LINK_GROUP:RESCAN,${SDL2TTF_STATIC_LIBRARIES} -lstdc++>)
add_static_library(sdl2_mixer SDL2MIXER)
pkg_check_modules(FREETYPE REQUIRED IMPORTED_TARGET freetype2)
if (XSYSTEM35_SDL_VERSION STREQUAL "2")
pkg_check_modules(SDL2 REQUIRED IMPORTED_TARGET sdl2>=2.18.0)
pkg_check_modules(SDL2MIXER REQUIRED IMPORTED_TARGET SDL2_mixer)
if (WIN32)
add_static_library(sdl2 SDL2)
add_static_library(sdl2_mixer SDL2MIXER)
target_link_libraries(xsystem35_sdl INTERFACE sdl2)
target_link_libraries(xsystem35_sdl_mixer INTERFACE sdl2_mixer)
else()
target_link_libraries(xsystem35_sdl INTERFACE PkgConfig::SDL2)
target_link_libraries(xsystem35_sdl_mixer INTERFACE
PkgConfig::SDL2MIXER)
endif()
else()
add_library(sdl2 ALIAS PkgConfig::SDL2)
add_library(sdl2_ttf ALIAS PkgConfig::SDL2TTF)
add_library(sdl2_mixer ALIAS PkgConfig::SDL2MIXER)
find_package(SDL3 3.4 REQUIRED CONFIG)
find_package(SDL3_mixer 3.2 QUIET CONFIG)
if (NOT SDL3_mixer_FOUND)
message(STATUS "SDL3_mixer not found; building SDL3_mixer 3.2.4 from source")
FetchContent_Declare(
SDL3_mixer
URL https://github.com/libsdl-org/SDL_mixer/archive/refs/tags/release-3.2.4.tar.gz
URL_HASH SHA256=f2ea848ccdf2f394cd4973ee0f6c482e04511044695cccfd46bab6dcd7f780aa
)
FetchContent_MakeAvailable(SDL3_mixer)
endif()
if (WIN32)
target_link_libraries(xsystem35_sdl INTERFACE SDL3::SDL3-static)
target_link_libraries(xsystem35_sdl_mixer INTERFACE
SDL3_mixer::SDL3_mixer-static)
else()
target_link_libraries(xsystem35_sdl INTERFACE SDL3::SDL3)
target_link_libraries(xsystem35_sdl_mixer INTERFACE
SDL3_mixer::SDL3_mixer)
endif()
endif()
if (WIN32)
add_static_library(freetype2 FREETYPE)
# harfbuzz and graphite2, which the static freetype depends on, are C++ libraries.
target_link_libraries(freetype2 INTERFACE -lstdc++)
else()
add_library(freetype2 ALIAS PkgConfig::FREETYPE)
set(DEFAULT_FONT_PATH ${CMAKE_INSTALL_PREFIX}/share/xsystem35/fonts/)
endif()
@@ -153,14 +202,19 @@ else() # non-emscripten, non-android
endif()
endif()
# Menu
# i18n support
#
# Translate via libintl if available; otherwise fall back to a built-in catalog
# compiled from the .po files.
#
# To add a language, create po/<lang>.po and list <lang> here.
set(NLS_LANGUAGES ja)
if (ENABLE_GTK)
# i18n support (currently only menus are translated)
if (NOT EMSCRIPTEN AND NOT ANDROID AND NOT WIN32)
include(FindIntl)
include(FindGettext)
if (Intl_FOUND AND GETTEXT_FOUND)
set(ENABLE_NLS 1)
set(HAVE_LIBINTL 1)
add_compile_definitions(LOCALEDIR="${CMAKE_INSTALL_PREFIX}/share/locale")
include_directories(${Intl_INCLUDE_DIRS})
link_libraries(${Intl_LIBRARIES})
@@ -211,6 +265,7 @@ endfunction()
message("xsystem35 summary:")
message("------------------")
print_summary(SDL ${XSYSTEM35_SDL_VERSION})
print_summary(audio ${SUMMARY_AUDIO})
print_summary(cdrom ${SUMMARY_CDROM})
print_summary(midi ${SUMMARY_MIDI})
@@ -227,9 +282,11 @@ add_subdirectory(fonts)
if (NOT ANDROID AND NOT EMSCRIPTEN)
add_subdirectory(doc)
endif()
if (ENABLE_NLS)
add_subdirectory(po)
endif()
add_subdirectory(po)
add_subdirectory(modules)
target_link_libraries(xsystem35 PRIVATE modules)
if (NOT ANDROID AND NOT EMSCRIPTEN)
add_subdirectory(test)
endif()
+66 -22
View File
@@ -1,4 +1,4 @@
# xsytem35-sdl2
# xsystem35 for SDL2/SDL3
This is a multi-platform port of `xsystem35`, a free implementation of
AliceSoft's System 3.x game engine.
@@ -102,53 +102,97 @@ $ xsystem35
See [xsystem35 command manual](doc/xsystem35.6.adoc) for detailed usage.
## Building
SDL2 is used by default on desktop platforms and Emscripten. Pass
`-DXSYSTEM35_SDL_VERSION=3` to CMake to select SDL3. Use a separate build
directory for each SDL version.
The supported configurations are:
| Platform | SDL2 | SDL3 |
| --- | --- | --- |
| Linux and macOS | Yes | Yes |
| Windows 64-bit | Yes | Yes |
| Windows 32-bit | Yes | No |
| Emscripten | Yes | Yes |
| Android | No | Yes |
SDL2 builds require SDL 2.18 or later and SDL2_mixer. SDL3 builds require
SDL 3.4 or later and SDL3_mixer 3.2 or later. The remaining common
dependencies are FreeType, zlib, and optionally libwebp, PortMidi, and cJSON.
### Linux (Debian / Ubuntu)
```bash
$ sudo apt install build-essential cmake libgtk-3-dev libsdl2-dev libsdl2-ttf-dev libsdl2-mixer-dev libwebp-dev libportmidi-dev libcjson-dev asciidoctor
$ mkdir -p out/debug
$ cd out/debug
$ cmake -DCMAKE_BUILD_TYPE=Debug ../../
$ make && make install
$ sudo apt install build-essential cmake libsdl2-dev libsdl2-mixer-dev \
libfreetype-dev libwebp-dev libportmidi-dev libcjson-dev asciidoctor
$ cmake -S . -B out/sdl2 -DCMAKE_BUILD_TYPE=Debug \
-DXSYSTEM35_SDL_VERSION=2
$ cmake --build out/sdl2
$ sudo cmake --install out/sdl2
```
For SDL3 on a distribution that provides SDL 3.4 or later (for example,
Ubuntu 26.04), install `libsdl3-dev` instead of the SDL2 packages and
configure `out/sdl3` with `-DXSYSTEM35_SDL_VERSION=3`. If SDL3_mixer 3.2 or
later is installed, CMake uses it; otherwise, CMake downloads and builds
SDL3_mixer 3.2.4 during configuration.
### MacOS
[Homebrew](https://brew.sh/) is required.
```bash
$ brew install cmake pkg-config sdl2 sdl2_mixer sdl2_ttf webp portmidi cjson asciidoctor
$ mkdir -p out/debug
$ cd out/debug
$ cmake -DCMAKE_BUILD_TYPE=Debug ../../
$ make && make install
$ brew install cmake pkg-config sdl2 sdl2_mixer freetype webp portmidi \
cjson asciidoctor
$ cmake -S . -B out/sdl2 -DCMAKE_BUILD_TYPE=Debug \
-DXSYSTEM35_SDL_VERSION=2
$ cmake --build out/sdl2
$ sudo cmake --install out/sdl2
```
For SDL3, install `sdl3 sdl3_mixer` instead of `sdl2 sdl2_mixer` and
configure `out/sdl3` with `-DXSYSTEM35_SDL_VERSION=3`.
### Windows
[MSYS2](https://www.msys2.org) is required.
```bash
$ pacman -S cmake mingw-w64-ucrt-x86_64-gcc mingw-w64-ucrt-x86_64-cmake mingw-w64-ucrt-x86_64-SDL2 mingw-w64-ucrt-x86_64-SDL2_ttf mingw-w64-ucrt-x86_64-SDL2_mixer mingw-w64-ucrt-x86_64-libwebp mingw-w64-ucrt-x86_64-portmidi mingw-w64-ucrt-x86_64-cjson
$ mkdir -p out/debug
$ cd out/debug
$ cmake -G"MSYS Makefiles" -DCMAKE_BUILD_TYPE=Debug ../../
$ make
$ pacman -S mingw-w64-ucrt-x86_64-gcc \
mingw-w64-ucrt-x86_64-cmake mingw-w64-ucrt-x86_64-SDL2 \
mingw-w64-ucrt-x86_64-SDL2_mixer mingw-w64-ucrt-x86_64-freetype \
mingw-w64-ucrt-x86_64-libwebp mingw-w64-ucrt-x86_64-portmidi \
mingw-w64-ucrt-x86_64-cjson
$ cmake -S . -B out/sdl2 -G"MSYS Makefiles" \
-DCMAKE_BUILD_TYPE=Debug -DXSYSTEM35_SDL_VERSION=2
$ cmake --build out/sdl2
```
For a 64-bit SDL3 build, install `mingw-w64-ucrt-x86_64-sdl3` and
`mingw-w64-ucrt-x86_64-sdl3-mixer` instead of the SDL2 packages, then
configure `out/sdl3` with `-DXSYSTEM35_SDL_VERSION=3`. The MSYS2 mingw32
repository does not provide SDL3, so 32-bit Windows builds remain on SDL2.
### Emscripten
```bash
$ mkdir -p out/wasm
$ cd out/wasm
$ emcmake cmake -DCMAKE_BUILD_TYPE=MinSizeRel ../../
$ make
$ emcmake cmake -S . -B out/wasm-sdl2 -DCMAKE_BUILD_TYPE=MinSizeRel \
-DXSYSTEM35_SDL_VERSION=2
$ cmake --build out/wasm-sdl2
```
For SDL3, use a different build directory and
`-DXSYSTEM35_SDL_VERSION=3`. Both versions use Emscripten's bundled SDL port;
SDL_mixer is not required.
To use the generated binary, check out
[Kichikuou on Web](https://github.com/kichikuou/web) and copy `out/xsystem35.*`
into its `docs` directory.
[Kichikuou on Web](https://github.com/kichikuou/web) and copy
`out/wasm-sdl2/src/xsystem35.*` into its `docs` directory.
### Android
See [android/README.md](android/).
Android uses SDL3 exclusively. The Gradle build downloads fixed official SDL3
3.4.16 and SDL3_mixer 3.2.4 AARs and exposes them to CMake through Prefab.
+27 -2
View File
@@ -16,13 +16,31 @@ Alternatively, you can install `xsystem35-sdl2` using [Obtainium](https://github
## Usage
### Basic Usage
You can install games either from a prepared ZIP file or directly from a CD-ROM
image.
#### Installing from a ZIP
1. Create a ZIP file containing all the game files and BGM files (see
[below](#preparing-a-zip) for details), and transfer it to your device.
2. Open the app. Tap the option menu (three dots in the top right corner) and
select "Install from ZIP".
3. Select the ZIP file you created in step 1.
4. The game will start. To simulate a right-click, tap the black bars on either
the left or right, or top or bottom of the screen.
4. The game will start.
#### Installing from a CD image
1. Transfer the CD image file(s) to your device.
2. Open the app. Tap the option menu and select "Install from CD image".
3. Select the image file and, when needed, the matching metadata file at the
same time. The supported combinations are:
- `.iso`
- `.bin` or `.img` with matching `.cue`
- `.bin` or `.img` with matching `.ccd`
- `.mdf` with matching `.mds`
In many Android file pickers, tapping a file immediately selects only that
one file. Long-press a file first to enter multi-select mode, then select
both the image file and its metadata file.
4. The game will start.
### Preparing a ZIP
- Include all files from the `GAMEDATA` folder (such as `.ALD` files and
@@ -41,6 +59,8 @@ Note: This ZIP format is also compatible with
[Kichikuou on Web](http://kichikuou.github.io/web/).
### Miscellaneous
- To simulate a right-click, tap the black bars on either the left or right,
or top or bottom of the screen.
- You can export or import save files via the game list's option menu.
- To uninstall a game, long-tap its title in the game list.
@@ -53,6 +73,11 @@ Note: This ZIP format is also compatible with
## Building from Source
Android builds use SDL3 exclusively. Gradle downloads the fixed official SDL3
3.4.16 and SDL3_mixer 3.2.4 AARs, verifies their checksums, and supplies their
native libraries to CMake through Prefab. No separate SDL installation is
required.
### Using Android Studio
Open this directory as an Android Studio project.
+74 -2
View File
@@ -8,19 +8,89 @@ else {
}
apply plugin: 'kotlin-android'
def aarsDir = file('aars')
def assetsLicensesDir = file('src/main/assets/licenses')
def sha256 = { File file ->
def digest = java.security.MessageDigest.getInstance('SHA-256')
file.withInputStream { input ->
byte[] buffer = new byte[8192]
for (int n = input.read(buffer); n >= 0; n = input.read(buffer)) {
digest.update(buffer, 0, n)
}
}
digest.digest().encodeHex().toString()
}
def installAar = { String url, String archiveSha256, String aarName,
String aarSha256, String licenseName ->
def aarFile = new File(aarsDir, aarName)
def licenseFile = new File(assetsLicensesDir, licenseName)
if (!aarFile.exists() || sha256(aarFile) != aarSha256 || !licenseFile.exists()) {
logger.lifecycle("Downloading ${aarName} ...")
def archive = File.createTempFile('sdl-android', '.zip')
try {
new URL(url).withInputStream { input ->
archive.withOutputStream { it << input }
}
if (sha256(archive) != archiveSha256) {
throw new GradleException("Checksum mismatch for ${url}")
}
aarsDir.mkdirs()
assetsLicensesDir.mkdirs()
copy {
from zipTree(archive)
include aarName
into aarsDir
}
copy {
from zipTree(archive)
include 'LICENSE.txt'
into assetsLicensesDir
rename 'LICENSE.txt', licenseName
}
} finally {
archive.delete()
}
}
if (!aarFile.exists() || sha256(aarFile) != aarSha256) {
throw new GradleException("Checksum mismatch for ${aarFile}")
}
aarFile
}
def sdlVersion = '3.4.16'
def sdlAarFile = installAar(
"https://github.com/libsdl-org/SDL/releases/download/release-${sdlVersion}/SDL3-devel-${sdlVersion}-android.zip",
'e1da8d9298471ea4ab8c6d48a804c0ac5415f35f30fcf7eba61aa7357e165765',
"SDL3-${sdlVersion}.aar",
'03710fc7b49cc070551446841a843840fabf2aaaaa3043cd5739292a55e4e61c',
'SDL')
def sdlMixerVersion = '3.2.4'
def sdlMixerAarFile = installAar(
"https://github.com/libsdl-org/SDL_mixer/releases/download/release-${sdlMixerVersion}/SDL3_mixer-devel-${sdlMixerVersion}-android.zip",
'b7f9f1ee5162a0492e71941f2cbdc50ec256ef15c0e995ca72c7e519eebd810d',
"SDL3_mixer-${sdlMixerVersion}.aar",
'd13dc45d4a4d6a2db4132400e77629c466ed533641fc9e211dfe85fab025b792',
'SDL_mixer')
android {
if (buildAsApplication) {
namespace "io.github.kichikuou.xsystem35"
}
buildFeatures {
prefab true
}
compileSdkVersion 34
defaultConfig {
minSdkVersion 21
targetSdkVersion 34
versionCode 36
versionName "2.18.0" + gitRevision()
versionName "2.20.0" + gitRevision()
externalNativeBuild {
cmake {
arguments "-DANDROID_APP_PLATFORM=android-19", "-DANDROID_STL=c++_static"
arguments "-DANDROID_STL=c++_static", "-DXSYSTEM35_SDL_VERSION=3"
// abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
abiFilters 'armeabi-v7a', 'arm64-v8a'
}
@@ -83,8 +153,10 @@ android {
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation files(sdlAarFile, sdlMixerAarFile)
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
testImplementation 'junit:junit:4.13.2'
}
static String gitRevision() {
-1
View File
@@ -1 +0,0 @@
include $(call all-subdir-makefiles)
-10
View File
@@ -1,10 +0,0 @@
# Uncomment this if you're using STL in your project
# You can find more information here:
# https://developer.android.com/ndk/guides/cpp-support
# APP_STL := c++_shared
APP_ABI := armeabi-v7a arm64-v8a x86 x86_64
# Min runtime API level
APP_PLATFORM=android-16
+17 -32
View File
@@ -3,37 +3,25 @@ cmake_minimum_required(VERSION 3.14)
project(GAME)
set(PROJECT_ROOT_DIR ../../..)
# Compilation of SDL and companion libraries
# SDL3 and SDL3_mixer are provided by their official Android AARs through Prefab.
find_package(SDL3 3.4 REQUIRED CONFIG)
find_package(SDL3_mixer 3.2.4 REQUIRED CONFIG)
include(FetchContent)
FetchContent_Declare(
SDL
URL https://github.com/libsdl-org/SDL/releases/download/release-2.32.10/SDL2-2.32.10.tar.gz
URL_HASH SHA1=ce98fa93e31836a751feca374ab28a0770b63c16
)
FetchContent_Declare(
SDL_ttf
URL https://github.com/libsdl-org/SDL_ttf/releases/download/release-2.22.0/SDL2_ttf-2.22.0.tar.gz
URL_HASH SHA1=da5e86b601ad299a697878fab1af6f3be47b529d
)
FetchContent_Declare(
SDL_mixer
URL https://github.com/libsdl-org/SDL_mixer/releases/download/release-2.8.0/SDL2_mixer-2.8.0.tar.gz
URL_HASH SHA1=a58c69f9d00e44833b9e00e1adb58d85759ca499
freetype
URL https://downloads.sourceforge.net/project/freetype/freetype2/2.14.3/freetype-2.14.3.tar.gz
https://download.savannah.gnu.org/releases/freetype/freetype-2.14.3.tar.gz
URL_HASH SHA1=a911f65c6355ddcf7aaba46e250dbc00f67e1678
)
# Only FreeType's own rasterizer is needed, none of its optional dependencies.
set(FT_DISABLE_ZLIB ON CACHE BOOL "Disable use of system zlib" FORCE)
set(FT_DISABLE_BZIP2 ON CACHE BOOL "Disable use of system libbz2" FORCE)
set(FT_DISABLE_PNG ON CACHE BOOL "Disable use of system libpng" FORCE)
set(FT_DISABLE_HARFBUZZ ON CACHE BOOL "Disable use of harfbuzz" FORCE)
set(FT_DISABLE_BROTLI ON CACHE BOOL "Disable use of brotli" FORCE)
set(SDL2TTF_SAMPLES OFF CACHE BOOL "Build the SDL2_ttf sample program(s)" FORCE)
set(SDL2TTF_INSTALL OFF CACHE BOOL "Enable SDL2_ttf install target" FORCE)
set(SDL2TTF_VENDORED ON CACHE BOOL "Use vendored third-party libraries" FORCE)
set(SDL2MIXER_OPUS OFF CACHE BOOL "Enable Opus music" FORCE)
set(SDL2MIXER_FLAC OFF CACHE BOOL "Enable FLAC music" FORCE)
set(SDL2MIXER_MOD OFF CACHE BOOL "Support loading MOD music" FORCE)
set(SDL2MIXER_MIDI OFF CACHE BOOL "Enable MIDI music" FORCE)
set(SDL2MIXER_WAVPACK OFF CACHE BOOL "Enable WavPack music" FORCE)
set(SDL2MIXER_SAMPLES OFF CACHE BOOL "Build the SDL2_mixer sample program(s)" FORCE)
set(SDL2MIXER_INSTALL OFF CACHE BOOL "Enable SDL2_mixer install target" FORCE)
FetchContent_MakeAvailable(SDL SDL_ttf SDL_mixer)
FetchContent_MakeAvailable(freetype)
# The main CMakeLists.txt of xsystem35
add_subdirectory(${PROJECT_ROOT_DIR} xsystem35)
@@ -47,8 +35,5 @@ file(COPY_FILE ${PROJECT_ROOT_DIR}/COPYING ${ASSETS_DIR}/licenses/xsystem35)
file(COPY_FILE ${PROJECT_ROOT_DIR}/licenses/MTLc3m.txt ${ASSETS_DIR}/licenses/MTLc3m)
file(COPY_FILE ${PROJECT_ROOT_DIR}/licenses/mincho.txt ${ASSETS_DIR}/licenses/mincho)
file(COPY_FILE ${PROJECT_ROOT_DIR}/licenses/nanojpeg.txt ${ASSETS_DIR}/licenses/nanojpeg)
file(COPY_FILE ${sdl_SOURCE_DIR}/LICENSE.txt ${ASSETS_DIR}/licenses/SDL)
file(COPY_FILE ${sdl_ttf_SOURCE_DIR}/LICENSE.txt ${ASSETS_DIR}/licenses/SDL_ttf)
file(COPY_FILE ${sdl_ttf_SOURCE_DIR}/external/freetype/docs/GPLv2.TXT ${ASSETS_DIR}/licenses/freetype)
file(COPY_FILE ${sdl_ttf_SOURCE_DIR}/external/harfbuzz/COPYING ${ASSETS_DIR}/licenses/harfbuzz)
file(COPY_FILE ${sdl_mixer_SOURCE_DIR}/LICENSE.txt ${ASSETS_DIR}/licenses/SDL_mixer)
file(COPY_FILE ${PROJECT_ROOT_DIR}/licenses/microui.txt ${ASSETS_DIR}/licenses/microui)
file(COPY_FILE ${freetype_SOURCE_DIR}/docs/GPLv2.TXT ${ASSETS_DIR}/licenses/freetype)
-18
View File
@@ -1,18 +0,0 @@
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := main
SDL_PATH := ../SDL
LOCAL_C_INCLUDES := $(LOCAL_PATH)/$(SDL_PATH)/include
# Add your application source files here...
LOCAL_SRC_FILES := YourSourceHere.c
LOCAL_SHARED_LIBRARIES := SDL2
LOCAL_LDLIBS := -lGLESv1_CM -lGLESv2 -lOpenSLES -llog -landroid
include $(BUILD_SHARED_LIBRARY)
-13
View File
@@ -1,13 +0,0 @@
cmake_minimum_required(VERSION 3.6)
project(MY_APP)
find_library(SDL2 SDL2)
add_library(main SHARED)
target_sources(main PRIVATE YourSourceHere.c)
target_link_libraries(main SDL2)
+2
View File
@@ -66,6 +66,7 @@
<!-- Example of setting SDL hints from AndroidManifest.xml:
<meta-data android:name="SDL_ENV.SDL_ACCELEROMETER_AS_JOYSTICK" android:value="0"/>
-->
<meta-data android:name="SDL_ENV.SDL_IOS_ORIENTATIONS" android:value="LandscapeLeft LandscapeRight"/>
<activity android:name=".LauncherActivity"
android:exported="true">
@@ -78,6 +79,7 @@
<activity android:name=".GameActivity"
android:label="@string/app_name"
android:process=":game"
android:alwaysRetainTaskState="true"
android:configChanges="layoutDirection|locale|orientation|uiMode|screenLayout|screenSize|smallestScreenSize|keyboard|keyboardHidden|navigation"
android:theme="@style/AppTheme"
@@ -0,0 +1,566 @@
/* Copyright (C) 2026 <KichikuouChrome@gmail.com>
*
* 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.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*
*/
package io.github.kichikuou.xsystem35
import android.content.ContentResolver
import android.os.ParcelFileDescriptor
import java.io.Closeable
import java.io.EOFException
import java.io.File
import java.io.FileInputStream
import java.io.OutputStream
import java.nio.ByteBuffer
import java.nio.channels.FileChannel
import java.util.Locale
internal class CdImageReader private constructor(
private val image: RandomAccessImage,
private val tracks: List<TrackInfo?>,
) : Closeable {
fun readSector(sector: Int, buffer: ByteArray) {
readDataFully(sector.toLong() * ISO_SECTOR_SIZE, buffer, 0, ISO_SECTOR_SIZE)
}
fun readDataFully(offset: Long, buffer: ByteArray, bufferOffset: Int, length: Int) {
// ISO9660 always sees logical 2048-byte data sectors even when the
// image stores each sector as a raw 2352-byte CD-ROM block.
var logicalOffset = offset
var outOffset = bufferOffset
var remaining = length
val sectorBuffer = ByteArray(ISO_SECTOR_SIZE)
while (remaining > 0) {
val sector = (logicalOffset / ISO_SECTOR_SIZE).toInt()
val offsetInSector = (logicalOffset % ISO_SECTOR_SIZE).toInt()
val count = minOf(remaining, ISO_SECTOR_SIZE - offsetInSector)
readSectorPayload(sector, sectorBuffer)
sectorBuffer.copyInto(buffer, outOffset, offsetInSector, offsetInSector + count)
logicalOffset += count
outOffset += count
remaining -= count
}
}
fun audioTracks(): List<Int> {
return tracks.mapIndexedNotNull { trackNumber, track ->
if (track?.isAudio == true) trackNumber else null
}
}
fun extractAudioTrack(trackNumber: Int, output: OutputStream) {
val track = tracks.getOrNull(trackNumber)
if (track?.isAudio != true) {
throw InstallFailureException(R.string.unsupported_cd_image)
}
val dataSize = track.numSectors.toLong() * RAW_AUDIO_SECTOR_SIZE
// CD-DA sectors are already 44.1kHz stereo 16-bit PCM; wrapping the
// raw sector stream in a WAV header makes it playable by SDL_mixer.
writeWaveHeader(output, dataSize)
val buffer = ByteArray(track.blockSize * 16)
var sector = 0
while (sector < track.numSectors) {
val sectorsToRead = minOf(16, track.numSectors - sector)
val bytesToRead = sectorsToRead * track.blockSize
image.readFully(track.offset + sector.toLong() * track.blockSize, buffer, 0, bytesToRead)
output.write(buffer, 0, sectorsToRead * RAW_AUDIO_SECTOR_SIZE)
sector += sectorsToRead
}
}
override fun close() {
image.close()
}
private fun readSectorPayload(sector: Int, buffer: ByteArray) {
val track = findDataTrack(sector)
val offset = track.offset + (sector - track.startSector).toLong() * track.blockSize + track.blockOffset
image.readFully(offset, buffer, 0, ISO_SECTOR_SIZE)
}
private fun findDataTrack(sector: Int): TrackInfo {
for (track in tracks) {
if (track == null || track.isAudio) {
continue
}
if (sector >= track.startSector && sector < track.startSector + track.numSectors) {
return track
}
}
throw InstallFailureException(R.string.invalid_iso9660_image)
}
companion object {
const val ISO_SECTOR_SIZE = 2048
private const val RAW_AUDIO_SECTOR_SIZE = 2352
private const val WAVE_HEADER_SIZE = 44
private const val MDS_MODE_AUDIO = 0xa9
private const val MDS_MODE_MODE1 = 0xaa
fun open(
contentResolver: ContentResolver,
files: List<SelectedInstallFile>,
tempDir: File,
): CdImageReader {
val selected = selectImageFiles(files)
val image = RandomAccessImage.open(contentResolver, selected.image, tempDir)
try {
if (selected.metadata == null) {
return CdImageReader(image, listOf(null, TrackInfo.iso(image.size)))
}
val metadataName = selected.metadata.displayName.lowercase(Locale.US)
val tracks = when {
metadataName.endsWith(".cue") -> {
val cueText = readMetadataText(contentResolver, selected.metadata)
parseCue(cueText, image.size)
}
metadataName.endsWith(".ccd") -> {
val ccdText = readMetadataText(contentResolver, selected.metadata)
parseCcd(ccdText, image.size)
}
metadataName.endsWith(".mds") -> {
val mdsBytes = readMetadataBytes(contentResolver, selected.metadata)
parseMds(mdsBytes)
}
else -> throw InstallFailureException(R.string.unsupported_cd_image)
}
return CdImageReader(image, tracks)
} catch (e: Exception) {
image.close()
throw e
}
}
internal fun openForFile(
imageFile: File,
metadataName: String? = null,
metadataBytes: ByteArray? = null,
): CdImageReader {
val image = RandomAccessImage.open(imageFile)
try {
if (metadataName == null) {
return CdImageReader(image, listOf(null, TrackInfo.iso(image.size)))
}
val tracks = when {
metadataName.lowercase(Locale.US).endsWith(".cue") -> {
parseCue(metadataBytes?.toString(Charsets.UTF_8) ?: "", image.size)
}
metadataName.lowercase(Locale.US).endsWith(".ccd") -> {
parseCcd(metadataBytes?.toString(Charsets.UTF_8) ?: "", image.size)
}
metadataName.lowercase(Locale.US).endsWith(".mds") -> {
parseMds(metadataBytes ?: ByteArray(0))
}
else -> throw InstallFailureException(R.string.unsupported_cd_image)
}
return CdImageReader(image, tracks)
} catch (e: Exception) {
image.close()
throw e
}
}
private fun selectImageFiles(files: List<SelectedInstallFile>): SelectedImageFiles {
if (files.isEmpty()) {
throw InstallFailureException(R.string.unsupported_cd_image)
}
val isoFiles = files.filter { it.displayName.lowercase(Locale.US).endsWith(".iso") }
if (isoFiles.size == 1 && files.size == 1) {
return SelectedImageFiles(isoFiles.single(), null)
}
val imageFiles = files.filter {
val name = it.displayName.lowercase(Locale.US)
name.endsWith(".bin") || name.endsWith(".img") || name.endsWith(".mdf")
}
val metadataFiles = files.filter {
val name = it.displayName.lowercase(Locale.US)
name.endsWith(".cue") || name.endsWith(".ccd") || name.endsWith(".mds")
}
if (imageFiles.size == 1 && metadataFiles.isEmpty() && files.size == 1 ||
imageFiles.isEmpty() && metadataFiles.size == 1 && files.size == 1
) {
throw InstallFailureException(R.string.missing_cd_image_metadata)
}
if (imageFiles.size != 1 || metadataFiles.size != 1 || files.size != 2) {
throw InstallFailureException(R.string.unsupported_cd_image)
}
val imageBase = imageFiles.single().baseName()
val metadataBase = metadataFiles.single().baseName()
if (imageBase != metadataBase) {
throw InstallFailureException(R.string.missing_cd_image_metadata)
}
if (!isSupportedMetadataForImage(imageFiles.single(), metadataFiles.single())) {
throw InstallFailureException(R.string.unsupported_cd_image)
}
return SelectedImageFiles(imageFiles.single(), metadataFiles.single())
}
private fun isSupportedMetadataForImage(
image: SelectedInstallFile,
metadata: SelectedInstallFile,
): Boolean {
val imageName = image.displayName.lowercase(Locale.US)
val metadataName = metadata.displayName.lowercase(Locale.US)
return if (imageName.endsWith(".mdf")) {
metadataName.endsWith(".mds")
} else {
metadataName.endsWith(".cue") || metadataName.endsWith(".ccd")
}
}
private fun readMetadataText(
contentResolver: ContentResolver,
file: SelectedInstallFile,
): String {
return contentResolver.openInputStream(file.uri)?.bufferedReader()?.use {
it.readText()
} ?: throw InstallFailureException(R.string.cd_image_read_error)
}
private fun readMetadataBytes(
contentResolver: ContentResolver,
file: SelectedInstallFile,
): ByteArray {
return contentResolver.openInputStream(file.uri)?.use {
it.readBytes()
} ?: throw InstallFailureException(R.string.cd_image_read_error)
}
private fun parseCue(cueText: String, imageSize: Long): List<TrackInfo?> {
val cueTracks = mutableListOf<CueTrack?>()
var currentTrack: Int? = null
for (line in cueText.lines()) {
val fields = line.trim().split(Regex("\\s+"))
if (fields.isEmpty()) {
continue
}
when (fields[0].uppercase(Locale.US)) {
"TRACK" -> {
if (fields.size < 3) {
throw InstallFailureException(R.string.unsupported_cd_image)
}
currentTrack = fields[1].toIntOrNull()
?: throw InstallFailureException(R.string.unsupported_cd_image)
while (cueTracks.size <= currentTrack) {
cueTracks.add(null)
}
cueTracks[currentTrack] = when (fields[2].uppercase(Locale.US)) {
"MODE1/2048" -> CueTrack(false, 2048, 0)
"MODE1/2352" -> CueTrack(false, 2352, 16)
"AUDIO" -> CueTrack(true, 2352, 0)
else -> throw InstallFailureException(R.string.unsupported_cd_image)
}
}
"INDEX" -> {
val trackNumber = currentTrack ?: continue
if (fields.size < 3) {
throw InstallFailureException(R.string.unsupported_cd_image)
}
val indexNumber = fields[1].toIntOrNull()
?: throw InstallFailureException(R.string.unsupported_cd_image)
cueTracks[trackNumber]?.index?.put(indexNumber, indexToSector(fields[2]))
}
}
}
return makeTrackInfo(cueTracks, imageSize)
}
private fun parseCcd(ccdText: String, imageSize: Long): List<TrackInfo?> {
val cueTracks = mutableListOf<CueTrack?>()
var currentTrack: Int? = null
for (line in ccdText.lines()) {
val trimmed = line.trim()
val trackMatch = Regex("""\[TRACK ([0-9]+)]""").matchEntire(trimmed)
if (trackMatch != null) {
currentTrack = trackMatch.groupValues[1].toInt()
while (cueTracks.size <= currentTrack) {
cueTracks.add(null)
}
cueTracks[currentTrack] = CueTrack(false, 2352, 16)
continue
}
val trackNumber = currentTrack ?: continue
val keyValue = trimmed.split("=", limit = 2)
if (keyValue.size != 2) {
continue
}
when (keyValue[0].uppercase(Locale.US)) {
"MODE" -> if (keyValue[1] == "0") {
cueTracks[trackNumber] = CueTrack(true, 2352, 0, cueTracks[trackNumber]?.index ?: mutableMapOf())
}
"INDEX 0" -> cueTracks[trackNumber]?.index?.put(
0,
keyValue[1].toIntOrNull() ?: throw InstallFailureException(R.string.unsupported_cd_image)
)
"INDEX 1" -> cueTracks[trackNumber]?.index?.put(
1,
keyValue[1].toIntOrNull() ?: throw InstallFailureException(R.string.unsupported_cd_image)
)
}
}
return makeTrackInfo(cueTracks, imageSize)
}
private fun parseMds(mdsBytes: ByteArray): List<TrackInfo?> {
if (mdsBytes.size < 0x70 ||
String(mdsBytes, 0, 16, Charsets.US_ASCII).trimEnd('\u0000') != "MEDIA DESCRIPTOR"
) {
throw InstallFailureException(R.string.unsupported_cd_image)
}
val entries = mdsBytes[0x62].toInt() and 0xff
if (0x70 + entries * 0x58 > mdsBytes.size) {
throw InstallFailureException(R.string.unsupported_cd_image)
}
val tracks = MutableList<TrackInfo?>(100) { null }
for (i in 0 until entries) {
val trackOffset = 0x70 + i * 0x50
val extraOffset = 0x70 + entries * 0x50 + i * 8
val mode = mdsBytes[trackOffset].toInt() and 0xff
val trackNumber = mdsBytes[trackOffset + 0x04].toInt() and 0xff
val sectorSize = readLittleEndianShort(mdsBytes, trackOffset + 0x10)
// MDS offsets are treated as 32-bit file offsets, so images
// beyond 4GB are unsupported.
val imageOffset = readLittleEndianInt(mdsBytes, trackOffset + 0x28)
val sectors = readLittleEndianInt(mdsBytes, extraOffset + 0x04).toInt()
if (trackNumber >= tracks.size) {
continue
}
tracks[trackNumber] = when (mode) {
MDS_MODE_AUDIO -> TrackInfo(
isAudio = true,
offset = imageOffset,
blockSize = sectorSize,
blockOffset = 0,
startSector = 0,
numSectors = sectors,
)
MDS_MODE_MODE1 -> TrackInfo(
isAudio = false,
offset = imageOffset,
blockSize = sectorSize,
blockOffset = 16,
startSector = 0,
numSectors = sectors,
)
else -> null
}
}
if (tracks.getOrNull(1)?.isAudio != false) {
throw InstallFailureException(R.string.unsupported_cd_image)
}
return tracks
}
private fun makeTrackInfo(cueTracks: List<CueTrack?>, imageSize: Long): List<TrackInfo?> {
val tracks = MutableList<TrackInfo?>(cueTracks.size) { null }
var offset = 0L
var startSector = 0
for (trackNumber in 1 until cueTracks.size) {
val cueTrack = cueTracks[trackNumber] ?: continue
val index1 = cueTrack.index[1]
?: throw InstallFailureException(R.string.unsupported_cd_image)
// INDEX 0 describes a pregap, but a zero INDEX 0 is treated as absent.
cueTrack.index[0]?.takeIf { it != 0 }?.let { index0 ->
val gap = index1 - index0
if (gap > 0) {
startSector += gap
offset += gap.toLong() * cueTrack.blockSize
}
}
val nextTrack = cueTracks.drop(trackNumber + 1).firstOrNull { it != null }
val nextStart = nextTrack?.let { it.index[0]?.takeIf { index -> index != 0 } ?: it.index[1] }
val numSectors = if (nextStart != null) {
nextStart - index1
} else {
((imageSize - offset) / cueTrack.blockSize).toInt()
}
if (numSectors <= 0) {
throw InstallFailureException(R.string.unsupported_cd_image)
}
tracks[trackNumber] = TrackInfo(
isAudio = cueTrack.isAudio,
offset = offset,
blockSize = cueTrack.blockSize,
blockOffset = cueTrack.blockOffset,
startSector = startSector,
numSectors = numSectors,
)
startSector += numSectors
offset += numSectors.toLong() * cueTrack.blockSize
}
return tracks
}
private fun indexToSector(index: String): Int {
val parts = index.split(":").map {
it.toIntOrNull() ?: throw InstallFailureException(R.string.unsupported_cd_image)
}
if (parts.size != 3) {
throw InstallFailureException(R.string.unsupported_cd_image)
}
return parts[0] * 60 * 75 + parts[1] * 75 + parts[2]
}
private fun writeWaveHeader(output: OutputStream, dataSize: Long) {
val header = ByteArray(WAVE_HEADER_SIZE)
writeAscii(header, 0, "RIFF")
writeLittleEndianInt(header, 4, dataSize + 36)
writeAscii(header, 8, "WAVE")
writeAscii(header, 12, "fmt ")
writeLittleEndianInt(header, 16, 16)
writeLittleEndianShort(header, 20, 1)
writeLittleEndianShort(header, 22, 2)
writeLittleEndianInt(header, 24, 44100)
writeLittleEndianInt(header, 28, 44100 * 2 * 2)
writeLittleEndianShort(header, 32, 2 * 2)
writeLittleEndianShort(header, 34, 16)
writeAscii(header, 36, "data")
writeLittleEndianInt(header, 40, dataSize)
output.write(header)
}
private fun writeAscii(buffer: ByteArray, offset: Int, value: String) {
value.toByteArray(Charsets.US_ASCII).copyInto(buffer, offset)
}
private fun writeLittleEndianShort(buffer: ByteArray, offset: Int, value: Int) {
buffer[offset] = value.toByte()
buffer[offset + 1] = (value shr 8).toByte()
}
private fun writeLittleEndianInt(buffer: ByteArray, offset: Int, value: Long) {
buffer[offset] = value.toByte()
buffer[offset + 1] = (value shr 8).toByte()
buffer[offset + 2] = (value shr 16).toByte()
buffer[offset + 3] = (value shr 24).toByte()
}
private fun readLittleEndianShort(buffer: ByteArray, offset: Int): Int {
return (buffer[offset].toInt() and 0xff) or
((buffer[offset + 1].toInt() and 0xff) shl 8)
}
private fun readLittleEndianInt(buffer: ByteArray, offset: Int): Long {
return (buffer[offset].toLong() and 0xff) or
((buffer[offset + 1].toLong() and 0xff) shl 8) or
((buffer[offset + 2].toLong() and 0xff) shl 16) or
((buffer[offset + 3].toLong() and 0xff) shl 24)
}
}
}
private data class SelectedImageFiles(
val image: SelectedInstallFile,
val metadata: SelectedInstallFile?,
)
private data class CueTrack(
val isAudio: Boolean,
val blockSize: Int,
val blockOffset: Int,
val index: MutableMap<Int, Int> = mutableMapOf(),
)
private data class TrackInfo(
val isAudio: Boolean,
val offset: Long,
val blockSize: Int,
val blockOffset: Int,
val startSector: Int,
val numSectors: Int,
) {
companion object {
fun iso(imageSize: Long): TrackInfo {
return TrackInfo(
isAudio = false,
offset = 0,
blockSize = CdImageReader.ISO_SECTOR_SIZE,
blockOffset = 0,
startSector = 0,
numSectors = (imageSize / CdImageReader.ISO_SECTOR_SIZE).toInt(),
)
}
}
}
private class RandomAccessImage private constructor(
private val channel: FileChannel,
private val descriptor: ParcelFileDescriptor?,
private val tempFile: File?,
) : Closeable {
val size: Long = channel.size()
fun readFully(offset: Long, buffer: ByteArray, bufferOffset: Int, length: Int) {
val byteBuffer = ByteBuffer.wrap(buffer, bufferOffset, length)
var pos = offset
while (byteBuffer.hasRemaining()) {
val read = channel.read(byteBuffer, pos)
if (read < 0) {
throw EOFException("Unexpected end of CD image")
}
pos += read
}
}
override fun close() {
channel.close()
descriptor?.close()
tempFile?.delete()
}
companion object {
internal fun open(file: File): RandomAccessImage {
return RandomAccessImage(FileInputStream(file).channel, null, null)
}
fun open(
contentResolver: ContentResolver,
file: SelectedInstallFile,
tempDir: File,
): RandomAccessImage {
val descriptor = contentResolver.openFileDescriptor(file.uri, "r")
?: throw InstallFailureException(R.string.cd_image_read_error)
try {
val channel = FileInputStream(descriptor.fileDescriptor).channel
channel.size()
return RandomAccessImage(channel, descriptor, null)
} catch (e: Exception) {
descriptor.close()
}
// Some SAF providers expose the URI as a pipe. Copy those to an
// app-private temp file so the CD reader can use positional reads.
val tempFile = File.createTempFile("cdimage-", ".img", tempDir)
try {
contentResolver.openInputStream(file.uri)?.use { input ->
tempFile.outputStream().buffered().use { output ->
input.copyTo(output)
}
} ?: throw InstallFailureException(R.string.cd_image_read_error)
return RandomAccessImage(FileInputStream(tempFile).channel, null, tempFile)
} catch (e: Exception) {
tempFile.delete()
throw e
}
}
}
}
private fun SelectedInstallFile.baseName(): String {
return displayName.substringBeforeLast('.').lowercase(Locale.US)
}
@@ -20,6 +20,7 @@ package io.github.kichikuou.xsystem35
import android.app.AlertDialog
import android.media.MediaPlayer
import android.os.Bundle
import android.os.Process
import android.text.InputType
import android.util.Log
import android.widget.EditText
@@ -44,6 +45,8 @@ class GameActivity : SDLActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
gameRoot = File(intent.getStringExtra(EXTRA_GAME_ROOT)!!)
// Workaround for https://github.com/libsdl-org/SDL/issues/8995
SDLActivity.setWindowStyle(true)
}
override fun onStop() {
@@ -56,8 +59,20 @@ class GameActivity : SDLActivity() {
midi.onActivityResume()
}
override fun onDestroy() {
try {
super.onDestroy()
} finally {
try {
midi.release()
} finally {
Process.killProcess(Process.myPid())
}
}
}
override fun getLibraries(): Array<String> {
return arrayOf("SDL2", "xsystem35")
return arrayOf("SDL3", "xsystem35")
}
override fun getArguments(): Array<String> {
@@ -75,7 +90,6 @@ class GameActivity : SDLActivity() {
return
}
File(gameRoot, Launcher.TITLE_FILE).writeText(str)
Launcher.updateGameList()
}
private fun textInputDialog(msg: String, oldVal: String, maxLen: Int, result: Array<String?>) {
@@ -154,11 +168,21 @@ class GameActivity : SDLActivity() {
}
private class MidiPlayer {
private enum class State {
STOPPED,
PLAYING,
PAUSED,
RELEASED,
}
private val player = MediaPlayer()
private var playing = false
private var playerPaused = false
private var state = State.STOPPED
fun start(path: String, loop: Boolean) {
if (state == State.RELEASED) {
return
}
state = State.STOPPED
try {
player.apply {
reset()
@@ -167,7 +191,7 @@ private class MidiPlayer {
prepare()
start()
}
playing = true
state = State.PLAYING
} catch (e: IOException) {
Log.e("midiStart", "Cannot play midi", e)
player.reset()
@@ -175,27 +199,41 @@ private class MidiPlayer {
}
fun stop() {
if (playing && player.isPlaying) {
player.stop()
playing = false
when (state) {
State.PLAYING, State.PAUSED -> {
player.stop()
state = State.STOPPED
}
State.STOPPED, State.RELEASED -> Unit
}
}
fun currentPosition(): Int {
return if (playing) player.currentPosition else 0
return when (state) {
State.PLAYING, State.PAUSED -> player.currentPosition
State.STOPPED, State.RELEASED -> 0
}
}
fun onActivityStop() {
if (playing && player.isPlaying) {
if (state == State.PLAYING && player.isPlaying) {
player.pause()
playerPaused = true
state = State.PAUSED
}
}
fun onActivityResume() {
if (playerPaused) {
if (state == State.PAUSED) {
player.start()
playerPaused = false
state = State.PLAYING
}
}
fun release() {
if (state == State.RELEASED) {
return
}
state = State.RELEASED
player.release()
}
}
@@ -0,0 +1,282 @@
/* Copyright (C) 2026 <KichikuouChrome@gmail.com>
*
* 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.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*
*/
package io.github.kichikuou.xsystem35
import android.content.ContentResolver
import android.net.Uri
import android.os.Build
import android.util.Log
import kotlinx.coroutines.runBlocking
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.InputStream
import java.io.UTFDataFormatException
import java.nio.charset.Charset
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
data class SelectedInstallFile(
val uri: Uri,
val displayName: String,
)
class GameInstaller(private val store: GameStore) {
suspend fun installZip(input: InputStream, progressCallback: suspend (String) -> Unit): File {
val dir = store.createDirForGame()
var committed = false
try {
val gameDir = extractFiles(input, dir, progressCallback)
committed = true
return gameDir
} finally {
if (!committed && !dir.deleteRecursively()) {
Log.w("launcher", "Failed to delete incomplete install directory: $dir")
}
}
}
suspend fun installCdImage(
files: List<SelectedInstallFile>,
contentResolver: ContentResolver,
tempDir: File,
progressCallback: suspend (String) -> Unit
): File {
val dir = store.createDirForGame()
var committed = false
try {
val gameDir = extractCdImage(files, contentResolver, tempDir, dir, progressCallback)
committed = true
return gameDir
} finally {
if (!committed && !dir.deleteRecursively()) {
Log.w("launcher", "Failed to delete incomplete install directory: $dir")
}
}
}
internal suspend fun installCdImageForTest(
cdImage: CdImageReader,
progressCallback: suspend (String) -> Unit
): File {
val dir = store.createDirForGame()
var committed = false
try {
val gameDir = extractCdImage(cdImage, dir, progressCallback)
committed = true
return gameDir
} finally {
if (!committed && !dir.deleteRecursively()) {
Log.w("launcher", "Failed to delete incomplete install directory: $dir")
}
}
}
private suspend fun extractFiles(
input: InputStream,
outDir: File,
progressCallback: suspend (String) -> Unit
): File {
val configWriter = GameConfigWriter()
val hadDecodeError = forEachZipEntrySuspending(input) { zipEntry, zip ->
Log.i("extractFiles", zipEntry.name)
if (zipEntry.isDirectory)
return@forEachZipEntrySuspending
val resolvedPath = resolveOutputPath(outDir, zipEntry.name)
val path = resolvedPath.file
path.parentFile?.mkdirs()
progressCallback(resolvedPath.relativePath)
FileOutputStream(path).buffered().use {
zip.copyTo(it)
}
configWriter.maybeAdd(resolvedPath.relativePath)
}
if (!configWriter.ready) {
if (hadDecodeError)
throw InstallFailureException(R.string.unsupported_zip)
throw InstallFailureException(R.string.cannot_find_ald)
}
configWriter.write(outDir)
return configWriter.gameDir?.let { File(outDir, it) } ?: outDir
}
private suspend fun extractCdImage(
files: List<SelectedInstallFile>,
contentResolver: ContentResolver,
tempDir: File,
outDir: File,
progressCallback: suspend (String) -> Unit
): File {
progressCallback("metadata parsing")
CdImageReader.open(contentResolver, files, tempDir).use { cdImage ->
return extractCdImage(cdImage, outDir, progressCallback)
}
}
private suspend fun extractCdImage(
cdImage: CdImageReader,
outDir: File,
progressCallback: suspend (String) -> Unit
): File {
val fs = Iso9660FileSystem(cdImage)
val gameData = fs.findGameDataDirectory()
?: throw InstallFailureException(R.string.cannot_find_game_data_directory)
var foundAld = false
fs.extractDirectory(gameData, "GAMEDATA") { path, input ->
val resolvedPath = resolveOutputPath(outDir, path)
resolvedPath.file.parentFile?.mkdirs()
progressCallback(resolvedPath.relativePath)
FileOutputStream(resolvedPath.file).buffered().use {
input.copyTo(it)
}
if (File(path).name.matches(""".*?s[a-z]\.ald""".toRegex(RegexOption.IGNORE_CASE))) {
foundAld = true
}
}
if (!foundAld) {
throw InstallFailureException(R.string.cannot_find_ald)
}
File(outDir, Launcher.GAMEDIR_FILE).writeText("GAMEDATA")
extractAudioTracks(cdImage, File(outDir, "GAMEDATA"), progressCallback)
return File(outDir, "GAMEDATA")
}
private suspend fun extractAudioTracks(
cdImage: CdImageReader,
gameDir: File,
progressCallback: suspend (String) -> Unit
) {
val audioTracks = cdImage.audioTracks()
if (audioTracks.isEmpty()) {
return
}
val cddaDir = File(gameDir, "cdda")
cddaDir.mkdirs()
val playlist = arrayOfNulls<String>(audioTracks.maxOrNull() ?: 0)
for (track in audioTracks) {
val relativePath = "cdda/track%02d.wav".format(track)
val outputFile = File(gameDir, relativePath)
progressCallback(relativePath)
FileOutputStream(outputFile).buffered().use {
cdImage.extractAudioTrack(track, it)
}
playlist[track - 1] = relativePath
}
File(gameDir, Launcher.PLAYLIST_FILE).writeText(playlist.joinToString("\n") { it ?: "" }.trimEnd('\n'))
}
}
internal class InstallFailureException(val msgId: Int) : Exception()
// A helper class which generates GAMEDIR_FILE and PLAYLIST_FILE.
private class GameConfigWriter {
var ready = false
private set
var gameDir: String? = null
private set
private val aldRegex = """.*?s[a-z]\.ald""".toRegex(RegexOption.IGNORE_CASE)
private val audioRegex = """((\d+).*|.*?(\d+))\.(wav|mp3|ogg)""".toRegex(RegexOption.IGNORE_CASE)
private val audioFiles: Array<String?> = arrayOfNulls(100)
fun maybeAdd(path: String) {
val name = File(path).name
aldRegex.matchEntire(name)?.let {
gameDir = File(path).parent
ready = true
}
audioRegex.matchEntire(name)?.let {
val track = it.groupValues[2].toIntOrNull() ?: it.groupValues[3].toInt()
if (0 < track && track <= audioFiles.size)
audioFiles[track - 1] = path
}
}
fun write(outDir: File) {
// Generate GAMEDIR_FILE
gameDir?.let {
File(outDir, Launcher.GAMEDIR_FILE).writeText(it)
}
// Generate PLAYLIST_FILE
val absGameDir = gameDir?.let { File(outDir, it) } ?: outDir
val playlistFile = File(absGameDir, Launcher.PLAYLIST_FILE)
if (!playlistFile.exists()) {
val prefixToRemove = gameDir?.let { "$it/" } ?: ""
val playlist = audioFiles.joinToString("\n") {
it?.removePrefix(prefixToRemove) ?: ""
}.trimEnd('\n')
playlistFile.writeText(playlist)
}
}
}
/**
* @property file Safe canonical output path.
* @property relativePath Path relative to the install root after canonicalization.
*/
internal data class ResolvedOutputPath(val file: File, val relativePath: String)
internal fun resolveOutputPath(baseDir: File, relativePath: String): ResolvedOutputPath {
if (File(relativePath).isAbsolute) {
throw IOException("Output path is absolute: $relativePath")
}
val canonicalBase = baseDir.canonicalFile
val file = File(canonicalBase, relativePath).canonicalFile
if (!isFileInsideDirectory(file, canonicalBase)) {
throw IOException("Output path is outside target directory: $relativePath")
}
val basePath = canonicalBase.path + File.separator
val canonicalRelativePath = file.path.removePrefix(basePath)
return ResolvedOutputPath(file, canonicalRelativePath)
}
private fun isFileInsideDirectory(file: File, directory: File): Boolean {
return file.path.startsWith(directory.path + File.separator)
}
internal fun forEachZipEntry(input: InputStream, action: (ZipEntry, ZipInputStream) -> Unit): Boolean =
runBlocking {
forEachZipEntrySuspending(input) { zipEntry, zip ->
action(zipEntry, zip)
}
}
internal suspend fun forEachZipEntrySuspending(
input: InputStream,
action: suspend (ZipEntry, ZipInputStream) -> Unit
): Boolean {
val zip = if (Build.VERSION.SDK_INT >= 24) {
ZipInputStream(input.buffered(), Charset.forName("Shift_JIS"))
} else {
ZipInputStream(input.buffered())
}
var hadDecodeError = false
zip.use {
while (true) {
try {
val zipEntry = zip.nextEntry ?: break
action(zipEntry, zip)
} catch (e: UTFDataFormatException) {
// Attempted to read Shift_JIS zip in Android < 7
Log.w("forEachZipEntry", "UTFDataFormatException: skipping a zip entry")
zip.closeEntry()
hadDecodeError = true
}
}
}
return hadDecodeError
}
@@ -0,0 +1,142 @@
/* Copyright (C) 2026 <KichikuouChrome@gmail.com>
*
* 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.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*
*/
package io.github.kichikuou.xsystem35
import android.util.Log
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.io.UTFDataFormatException
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream
class GameStore(private val rootDir: File) {
data class Entry(val path: File, val title: String, val timestamp: Long)
private val gameList = arrayListOf<Entry>()
val games: List<Entry>
get() = gameList
val titles: List<String>
get() = gameList.map(Entry::title)
init {
updateGameList()
}
fun uninstall(id: Int) {
gameList[id].path.deleteRecursively()
gameList.removeAt(id)
}
fun updateGameList() {
var saveDirFound = false
gameList.clear()
for (path in rootDir.listFiles() ?: emptyArray()) {
if (!path.isDirectory)
continue
if (path.name == Launcher.SAVE_DIR) {
saveDirFound = true
continue
}
try {
val gameDirFile = File(path, Launcher.GAMEDIR_FILE)
val gamePath = if (gameDirFile.exists()) File(path, gameDirFile.readText()) else path
val titleFile = File(gamePath, Launcher.TITLE_FILE)
val title = titleFile.readText()
gameList.add(Entry(gamePath, title, titleFile.lastModified()))
migratePlaylist(path)
} catch (e: IOException) {
// Incomplete game installation. Delete it.
path.deleteRecursively()
}
}
gameList.sortByDescending(Entry::timestamp)
if (!saveDirFound) {
File(rootDir, Launcher.SAVE_DIR).mkdir()
}
}
fun createDirForGame(): File {
var i = 0
while (true) {
val f = File(rootDir, i++.toString())
if (!f.exists() && f.mkdir()) {
return f
}
}
}
// Throws IOException
fun exportSaveData(output: OutputStream) {
ZipOutputStream(output.buffered()).use { zip ->
for (path in File(rootDir, Launcher.SAVE_DIR).listFiles() ?: emptyArray()) {
if (path.isDirectory || path.name.endsWith(".asd."))
continue
val pathInZip = "${Launcher.SAVE_DIR}/${path.name}"
Log.i("exportSaveData", pathInZip)
zip.putNextEntry(ZipEntry(pathInZip))
path.inputStream().buffered().use {
it.copyTo(zip)
}
}
}
}
fun importSaveData(input: InputStream): Int? {
try {
var imported = false
forEachZipEntry(input) { zipEntry, zip ->
// Process only files directly under save/
if (zipEntry.isDirectory || !zipEntry.name.startsWith("save/") ||
zipEntry.name.count{it == '/'} != 1)
return@forEachZipEntry
val path = resolveOutputPath(rootDir, zipEntry.name).file
Log.i("importSaveData", zipEntry.name)
FileOutputStream(path).buffered().use {
zip.copyTo(it)
}
imported = true
}
return if (imported) null else R.string.no_data_to_import
} catch (e: UTFDataFormatException) {
// Attempted to read Shift_JIS zip in Android < 7
return R.string.unsupported_zip
} catch (e: IOException) {
Log.e("launcher", "Failed to extract ZIP", e)
return R.string.zip_extraction_error
}
}
fun clearSaveData(): Boolean {
var success = true
File(rootDir, Launcher.SAVE_DIR).listFiles()?.forEach { file ->
if (!file.deleteRecursively()) success = false
}
return success
}
// Xsystem35-sdl2 2.3.0 - 2.11.1 used playlist2.txt. Rename it to playlist.txt.
private fun migratePlaylist(dir: File) {
val oldPlaylist = File(dir, Launcher.OLD_PLAYLIST_FILE)
if (oldPlaylist.exists()) {
oldPlaylist.renameTo(File(dir, Launcher.PLAYLIST_FILE))
}
}
}
@@ -0,0 +1,175 @@
/* Copyright (C) 2026 <KichikuouChrome@gmail.com>
*
* 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.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*
*/
package io.github.kichikuou.xsystem35
import java.io.InputStream
import java.nio.charset.Charset
import java.util.Locale
internal class Iso9660FileSystem(private val image: CdImageReader) {
private val descriptor = readBestVolumeDescriptor()
data class Entry(
val name: String,
val extent: Int,
val size: Int,
val isDirectory: Boolean,
)
fun findGameDataDirectory(): Entry? {
return listDirectory(descriptor.rootDirectory).firstOrNull {
it.isDirectory && it.name.uppercase(Locale.US) == "GAMEDATA"
}
}
suspend fun extractDirectory(
directory: Entry,
outputPath: String,
writeFile: suspend (String, InputStream) -> Unit,
) {
for (entry in listDirectory(directory)) {
val childPath = "$outputPath/${entry.name}"
if (entry.isDirectory) {
extractDirectory(entry, childPath, writeFile)
} else {
writeFile(childPath, fileInputStream(entry))
}
}
}
private fun listDirectory(directory: Entry): List<Entry> {
val bytes = ByteArray(directory.size)
image.readDataFully(
directory.extent.toLong() * CdImageReader.ISO_SECTOR_SIZE,
bytes,
0,
bytes.size
)
val entries = mutableListOf<Entry>()
var offset = 0
while (offset < bytes.size) {
val length = bytes[offset].toInt() and 0xff
if (length == 0) {
offset = ((offset / CdImageReader.ISO_SECTOR_SIZE) + 1) * CdImageReader.ISO_SECTOR_SIZE
continue
}
parseDirectoryRecord(bytes, offset, length)?.let { entries.add(it) }
offset += length
}
return entries
}
private fun parseDirectoryRecord(bytes: ByteArray, offset: Int, length: Int): Entry? {
if (length < 34) {
throw InstallFailureException(R.string.invalid_iso9660_image)
}
val nameLength = bytes[offset + 32].toInt() and 0xff
if (33 + nameLength > length) {
throw InstallFailureException(R.string.invalid_iso9660_image)
}
val rawName = bytes.copyOfRange(offset + 33, offset + 33 + nameLength)
if (rawName.size == 1 && (rawName[0].toInt() == 0 || rawName[0].toInt() == 1)) {
return null
}
val name = descriptor.decodeName(rawName)
val extent = readLittleEndianInt(bytes, offset + 2)
val size = readLittleEndianInt(bytes, offset + 10)
val flags = bytes[offset + 25].toInt() and 0xff
return Entry(name, extent, size, flags and 0x02 != 0)
}
private fun fileInputStream(entry: Entry): InputStream {
return object : InputStream() {
private var pos = 0
override fun read(): Int {
val buffer = ByteArray(1)
val read = read(buffer, 0, 1)
return if (read < 0) -1 else buffer[0].toInt() and 0xff
}
override fun read(buffer: ByteArray, offset: Int, length: Int): Int {
if (pos >= entry.size) {
return -1
}
val count = minOf(length, entry.size - pos)
image.readDataFully(
entry.extent.toLong() * CdImageReader.ISO_SECTOR_SIZE + pos,
buffer,
offset,
count
)
pos += count
return count
}
}
}
private fun readBestVolumeDescriptor(): VolumeDescriptor {
var primary: VolumeDescriptor? = null
var joliet: VolumeDescriptor? = null
val sector = ByteArray(CdImageReader.ISO_SECTOR_SIZE)
var sectorNumber = 0x10
while (true) {
image.readSector(sectorNumber++, sector)
if (String(sector, 1, 5, Charsets.US_ASCII) != "CD001") {
throw InstallFailureException(R.string.invalid_iso9660_image)
}
when (sector[0].toInt() and 0xff) {
1 -> primary = VolumeDescriptor(parseRootDirectory(sector), false)
2 -> if (isJolietDescriptor(sector)) {
joliet = VolumeDescriptor(parseRootDirectory(sector), true)
}
255 -> return joliet ?: primary
?: throw InstallFailureException(R.string.invalid_iso9660_image)
}
}
}
private fun parseRootDirectory(sector: ByteArray): Entry {
return Entry(
name = "",
extent = readLittleEndianInt(sector, 156 + 2),
size = readLittleEndianInt(sector, 156 + 10),
isDirectory = true,
)
}
private fun isJolietDescriptor(sector: ByteArray): Boolean {
return sector[88] == 0x25.toByte() &&
sector[89] == 0x2f.toByte() &&
(sector[90] == 0x40.toByte() || sector[90] == 0x43.toByte() || sector[90] == 0x45.toByte())
}
private data class VolumeDescriptor(
val rootDirectory: Entry,
val joliet: Boolean,
) {
fun decodeName(rawName: ByteArray): String {
val charset = if (joliet) Charsets.UTF_16BE else Charset.forName("Shift_JIS")
return String(rawName, charset).substringBefore(";")
}
}
}
private fun readLittleEndianInt(bytes: ByteArray, offset: Int): Int {
return (bytes[offset].toInt() and 0xff) or
((bytes[offset + 1].toInt() and 0xff) shl 8) or
((bytes[offset + 2].toInt() and 0xff) shl 16) or
((bytes[offset + 3].toInt() and 0xff) shl 24)
}
@@ -17,18 +17,15 @@
*/
package io.github.kichikuou.xsystem35
import android.os.Build
import android.content.ContentResolver
import android.util.Log
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.*
import java.nio.charset.Charset
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
import java.util.zip.ZipOutputStream
private var gLauncher: Launcher? = null
@@ -39,7 +36,16 @@ interface LauncherObserver {
fun onInstallFailure(msgId: Int)
}
class Launcher private constructor(private val rootDir: File) {
sealed class InstallState {
object Idle : InstallState()
data class Installing(val progress: String?) : InstallState()
data class Succeeded(val path: File, val archiveName: String?) : InstallState()
data class Failed(val msgId: Int) : InstallState()
}
data class InstallResult(val path: File, val archiveName: String?)
class Launcher private constructor(rootDir: File) {
companion object {
const val SAVE_DIR = "save"
const val TITLE_FILE = "title.txt"
@@ -54,234 +60,118 @@ class Launcher private constructor(private val rootDir: File) {
return gLauncher!!
}
fun updateGameList() {
gLauncher?.updateGameList()
}
}
data class Entry(val path: File, val title: String, val timestamp: Long)
val games = arrayListOf<Entry>()
private val store = GameStore(rootDir)
private val installer = GameInstaller(store)
val games: List<GameStore.Entry>
get() = store.games
val titles: List<String>
get() = games.map(Entry::title)
get() = store.titles
var observer: LauncherObserver? = null
var isInstalling = false
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
private var installJob: Job? = null
var installState: InstallState = InstallState.Idle
private set
init {
updateGameList()
}
@OptIn(DelicateCoroutinesApi::class)
fun install(input: InputStream, archiveName: String?) {
val dir = createDirForGame()
isInstalling = true
GlobalScope.launch(Dispatchers.Main) {
try {
val gameDir = withContext(Dispatchers.IO) {
extractFiles(input, dir) { msg ->
GlobalScope.launch(Dispatchers.Main) {
observer?.onInstallProgress(msg)
fun installZip(input: InputStream, archiveName: String?) {
if (installJob?.isActive == true) {
input.close()
return
}
startInstallJob {
val gameDir = withContext(Dispatchers.IO) {
input.use {
installer.installZip(it) { msg ->
withContext(Dispatchers.Main) {
setInstallProgress(msg)
}
}
}
observer?.onInstallSuccess(gameDir, archiveName)
} catch (e: InstallFailureException) {
observer?.onInstallFailure(e.msgId)
} catch (e: Exception) {
Log.e("launcher", "Failed to extract ZIP", e)
observer?.onInstallFailure(R.string.zip_extraction_error)
}
isInstalling = false
InstallResult(gameDir, archiveName)
}
}
fun installCdImage(
files: List<SelectedInstallFile>,
contentResolver: ContentResolver,
tempDir: File,
) {
if (installJob?.isActive == true) {
return
}
startInstallJob {
val gameDir = withContext(Dispatchers.IO) {
installer.installCdImage(files, contentResolver, tempDir) { msg ->
withContext(Dispatchers.Main) {
setInstallProgress(msg)
}
}
}
InstallResult(gameDir, null)
}
}
private fun startInstallJob(block: suspend () -> InstallResult) {
if (installJob?.isActive == true) {
return
}
installState = InstallState.Installing(null)
installJob = scope.launch {
try {
val result = block()
setInstallSucceeded(result)
} catch (e: InstallFailureException) {
setInstallFailed(e.msgId)
} catch (e: Exception) {
Log.e("launcher", "Failed to install game", e)
setInstallFailed(R.string.install_error)
}
}
}
fun consumeInstallResult() {
if (installState is InstallState.Succeeded || installState is InstallState.Failed) {
installState = InstallState.Idle
}
}
private fun setInstallProgress(progress: String) {
installState = InstallState.Installing(progress)
observer?.onInstallProgress(progress)
}
private fun setInstallSucceeded(result: InstallResult) {
installState = InstallState.Succeeded(result.path, result.archiveName)
observer?.onInstallSuccess(result.path, result.archiveName)
}
private fun setInstallFailed(msgId: Int) {
installState = InstallState.Failed(msgId)
observer?.onInstallFailure(msgId)
}
fun uninstall(id: Int) {
games[id].path.deleteRecursively()
games.removeAt(id)
store.uninstall(id)
observer?.onGameListChange()
}
private fun updateGameList() {
var saveDirFound = false
games.clear()
for (path in rootDir.listFiles() ?: emptyArray()) {
if (!path.isDirectory)
continue
if (path.name == SAVE_DIR) {
saveDirFound = true
continue
}
try {
val gameDirFile = File(path, GAMEDIR_FILE)
val gamePath = if (gameDirFile.exists()) File(path, gameDirFile.readText()) else path
val titleFile = File(gamePath, TITLE_FILE)
val title = titleFile.readText()
games.add(Entry(gamePath, title, titleFile.lastModified()))
migratePlaylist(path)
} catch (e: IOException) {
// Incomplete game installation. Delete it.
path.deleteRecursively()
}
}
games.sortByDescending(Entry::timestamp)
if (!saveDirFound) {
File(rootDir, SAVE_DIR).mkdir()
}
fun refreshGameList() {
store.updateGameList()
observer?.onGameListChange()
}
private fun createDirForGame(): File {
var i = 0
while (true) {
val f = File(rootDir, i++.toString())
if (!f.exists() && f.mkdir()) {
return f
}
}
}
// Throws IOException
fun exportSaveData(output: OutputStream) {
ZipOutputStream(output.buffered()).use { zip ->
for (path in File(rootDir, SAVE_DIR).listFiles() ?: emptyArray()) {
if (path.isDirectory || path.name.endsWith(".asd."))
continue
val pathInZip = "${SAVE_DIR}/${path.name}"
Log.i("exportSaveData", pathInZip)
zip.putNextEntry(ZipEntry(pathInZip))
path.inputStream().buffered().use {
it.copyTo(zip)
}
}
}
store.exportSaveData(output)
}
fun importSaveData(input: InputStream): Int? {
try {
var imported = false
forEachZipEntry(input) { zipEntry, zip ->
// Process only files directly under save/
if (zipEntry.isDirectory || !zipEntry.name.startsWith("save/") ||
zipEntry.name.count{it == '/'} != 1)
return@forEachZipEntry
Log.i("importSaveData", zipEntry.name)
FileOutputStream(File(rootDir, zipEntry.name)).buffered().use {
zip.copyTo(it)
}
imported = true
}
return if (imported) null else R.string.no_data_to_import
} catch (e: UTFDataFormatException) {
// Attempted to read Shift_JIS zip in Android < 7
return R.string.unsupported_zip
} catch (e: IOException) {
Log.e("launcher", "Failed to extract ZIP", e)
return R.string.zip_extraction_error
}
}
private fun extractFiles(input: InputStream, outDir: File, progressCallback: (String) -> Unit): File {
val configWriter = GameConfigWriter()
val hadDecodeError = forEachZipEntry(input) { zipEntry, zip ->
Log.i("extractFiles", zipEntry.name)
val path = File(outDir, zipEntry.name)
if (zipEntry.isDirectory)
return@forEachZipEntry
path.parentFile?.mkdirs()
progressCallback(zipEntry.name)
FileOutputStream(path).buffered().use {
zip.copyTo(it)
}
configWriter.maybeAdd(zipEntry.name)
}
if (!configWriter.ready) {
if (hadDecodeError)
throw InstallFailureException(R.string.unsupported_zip)
throw InstallFailureException(R.string.cannot_find_ald)
}
configWriter.write(outDir)
return configWriter.gameDir?.let { File(outDir, it) } ?: outDir
}
// Xsystem35-sdl2 2.3.0 - 2.11.1 used playlist2.txt. Rename it to playlist.txt.
private fun migratePlaylist(dir: File) {
val oldPlaylist = File(dir, OLD_PLAYLIST_FILE)
if (oldPlaylist.exists()) {
oldPlaylist.renameTo(File(dir, PLAYLIST_FILE))
}
}
class InstallFailureException(val msgId: Int) : Exception()
// A helper class which generates GAMEDIR_FILE and PLAYLIST_FILE.
private class GameConfigWriter {
var ready = false
private set
var gameDir: String? = null
private set
private val aldRegex = """.*?s[a-z]\.ald""".toRegex(RegexOption.IGNORE_CASE)
private val audioRegex = """((\d+).*|.*?(\d+))\.(wav|mp3|ogg)""".toRegex(RegexOption.IGNORE_CASE)
private val audioFiles: Array<String?> = arrayOfNulls(100)
fun maybeAdd(path: String) {
val name = File(path).name
aldRegex.matchEntire(name)?.let {
gameDir = File(path).parent
ready = true
}
audioRegex.matchEntire(name)?.let {
val track = it.groupValues[2].toIntOrNull() ?: it.groupValues[3].toInt()
if (0 < track && track <= audioFiles.size)
audioFiles[track - 1] = path
}
}
fun write(outDir: File) {
// Generate GAMEDIR_FILE
gameDir?.let {
File(outDir, GAMEDIR_FILE).writeText(it)
}
// Generate PLAYLIST_FILE
val absGameDir = gameDir?.let { File(outDir, it) } ?: outDir
val playlistFile = File(absGameDir, PLAYLIST_FILE)
if (!playlistFile.exists()) {
val prefixToRemove = gameDir?.let { "$it/" } ?: ""
val playlist = audioFiles.joinToString("\n") {
it?.removePrefix(prefixToRemove) ?: ""
}.trimEnd('\n')
playlistFile.writeText(playlist)
}
}
return store.importSaveData(input)
}
fun clearSaveData(): Boolean {
var success = true
File(rootDir, SAVE_DIR).listFiles()?.forEach { file ->
if (!file.deleteRecursively()) success = false
}
return success
return store.clearSaveData()
}
}
private fun forEachZipEntry(input: InputStream, action: (ZipEntry, ZipInputStream) -> Unit): Boolean {
val zip = if (Build.VERSION.SDK_INT >= 24) {
ZipInputStream(input.buffered(), Charset.forName("Shift_JIS"))
} else {
ZipInputStream(input.buffered())
}
var hadDecodeError = false
zip.use {
while (true) {
try {
val zipEntry = zip.nextEntry ?: break
action(zipEntry, zip)
} catch (e: UTFDataFormatException) {
// Attempted to read Shift_JIS zip in Android < 7
Log.w("forEachZipEntry", "UTFDataFormatException: skipping a zip entry")
zip.closeEntry()
hadDecodeError = true
}
}
}
return hadDecodeError
}
@@ -33,10 +33,10 @@ import android.widget.Toast
import java.io.*
private const val CONTENT_TYPE_ZIP = "application/zip"
private const val INSTALL_REQUEST = 1
private const val ZIP_INSTALL_REQUEST = 1
private const val SAVEDATA_EXPORT_REQUEST = 2
private const val SAVEDATA_IMPORT_REQUEST = 3
private const val STATE_PROGRESS_TEXT = "progressText"
private const val CD_IMAGE_INSTALL_REQUEST = 4
class LauncherActivity : Activity(), LauncherObserver {
private lateinit var launcher: Launcher
@@ -48,9 +48,7 @@ class LauncherActivity : Activity(), LauncherObserver {
launcher = Launcher.getInstance(filesDir)
launcher.observer = this
if (launcher.isInstalling) {
showProgressDialog(savedInstanceState)
}
renderInstallState(launcher.installState)
onGameListChange()
val listView = findViewById<ListView>(R.id.list)
@@ -70,11 +68,9 @@ class LauncherActivity : Activity(), LauncherObserver {
super.onDestroy()
}
override fun onSaveInstanceState(outState: Bundle) {
progressDialog?.let {
outState.putCharSequence(STATE_PROGRESS_TEXT, it.findViewById<TextView>(R.id.text).text)
}
super.onSaveInstanceState(outState)
override fun onResume() {
super.onResume()
launcher.refreshGameList()
}
private fun onListItemClick(position: Int) {
@@ -100,7 +96,15 @@ class LauncherActivity : Activity(), LauncherObserver {
R.id.install_from_zip -> {
val i = Intent(Intent.ACTION_GET_CONTENT)
i.type = CONTENT_TYPE_ZIP
startActivityForResult(Intent.createChooser(i, getString(R.string.choose_a_file)), INSTALL_REQUEST)
startActivityForResult(Intent.createChooser(i, getString(R.string.choose_a_file)), ZIP_INSTALL_REQUEST)
true
}
R.id.install_from_cd_image -> {
val i = Intent(Intent.ACTION_GET_CONTENT)
i.type = "*/*"
i.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
startActivityForResult(Intent.createChooser(i, getString(R.string.choose_cd_image_files)),
CD_IMAGE_INSTALL_REQUEST)
true
}
R.id.export_savedata -> {
@@ -150,14 +154,20 @@ class LauncherActivity : Activity(), LauncherObserver {
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (resultCode != RESULT_OK)
return
val uri = data?.data ?: return
when (requestCode) {
INSTALL_REQUEST -> {
ZIP_INSTALL_REQUEST -> {
val uri = data?.data ?: return
val input = contentResolver.openInputStream(uri) ?: return
showProgressDialog()
launcher.install(input, getArchiveName(uri))
launcher.installZip(input, getArchiveName(uri))
renderInstallState(launcher.installState)
}
CD_IMAGE_INSTALL_REQUEST -> {
val files = getSelectedInstallFiles(data)
launcher.installCdImage(files, contentResolver, cacheDir)
renderInstallState(launcher.installState)
}
SAVEDATA_EXPORT_REQUEST -> try {
val uri = data?.data ?: return
launcher.exportSaveData(contentResolver.openOutputStream(uri)!!)
Toast.makeText(this, R.string.save_data_export_success, Toast.LENGTH_SHORT).show()
} catch (e: IOException) {
@@ -165,6 +175,7 @@ class LauncherActivity : Activity(), LauncherObserver {
errorDialog(R.string.save_data_export_error)
}
SAVEDATA_IMPORT_REQUEST -> {
val uri = data?.data ?: return
val input = contentResolver.openInputStream(uri) ?: return
val errMsgId = launcher.importSaveData(input)
if (errMsgId == null) {
@@ -184,17 +195,15 @@ class LauncherActivity : Activity(), LauncherObserver {
}
override fun onInstallProgress(path: String) {
progressDialog?.findViewById<TextView>(R.id.text)?.text = getString(R.string.install_progress, path)
renderInstallState(launcher.installState)
}
override fun onInstallSuccess(path: File, archiveName: String?) {
dismissProgressDialog()
startGame(path, archiveName)
renderInstallState(launcher.installState)
}
override fun onInstallFailure(msgId: Int) {
dismissProgressDialog()
errorDialog(msgId)
renderInstallState(launcher.installState)
}
private fun startGame(path: File, archiveName: String?) {
@@ -210,16 +219,35 @@ class LauncherActivity : Activity(), LauncherObserver {
launcher.uninstall(id)
}
private fun showProgressDialog(savedInstanceState: Bundle? = null) {
progressDialog = Dialog(this)
progressDialog!!.apply {
setTitle(R.string.install_dialog_title)
setCancelable(false)
setContentView(R.layout.progress_dialog)
savedInstanceState?.let {
findViewById<TextView>(R.id.text)?.text = it.getCharSequence(STATE_PROGRESS_TEXT)
private fun renderInstallState(state: InstallState) {
when (state) {
InstallState.Idle -> dismissProgressDialog()
is InstallState.Installing -> showProgressDialog(state.progress)
is InstallState.Succeeded -> {
dismissProgressDialog()
startGame(state.path, state.archiveName)
launcher.consumeInstallResult()
}
show()
is InstallState.Failed -> {
dismissProgressDialog()
errorDialog(state.msgId)
launcher.consumeInstallResult()
}
}
}
private fun showProgressDialog(progress: String? = null) {
if (progressDialog == null) {
progressDialog = Dialog(this)
progressDialog!!.apply {
setTitle(R.string.install_dialog_title)
setCancelable(false)
setContentView(R.layout.progress_dialog)
show()
}
}
progress?.let {
progressDialog?.findViewById<TextView>(R.id.text)?.text = getString(R.string.install_progress, it)
}
}
@@ -236,13 +264,33 @@ class LauncherActivity : Activity(), LauncherObserver {
}
private fun getArchiveName(uri: Uri): String? {
val fname = getDisplayName(uri) ?: return null
return if (fname.endsWith(".zip", true)) fname.dropLast(4) else fname
}
private fun getSelectedInstallFiles(data: Intent?): List<SelectedInstallFile> {
val files = mutableListOf<SelectedInstallFile>()
val clipData = data?.clipData
if (clipData != null) {
for (i in 0 until clipData.itemCount) {
val uri = clipData.getItemAt(i).uri
files.add(SelectedInstallFile(uri, getDisplayName(uri) ?: uri.lastPathSegment ?: ""))
}
} else {
data?.data?.let { uri ->
files.add(SelectedInstallFile(uri, getDisplayName(uri) ?: uri.lastPathSegment ?: ""))
}
}
return files
}
private fun getDisplayName(uri: Uri): String? {
val cursor = contentResolver.query(uri, null, null, null, null, null)
cursor?.use {
if (it.moveToFirst()) {
val column = it.getColumnIndex(OpenableColumns.DISPLAY_NAME)
if (column >= 0) {
val fname = it.getString(column)
return if (fname.endsWith(".zip", true)) fname.dropLast(4) else fname
return it.getString(column)
}
}
}
@@ -12,10 +12,9 @@ class LicensesMenuActivity : Activity() {
Entry("xsystem35-sdl2", "xsystem35", "https://github.com/kichikuou/xsystem35-sdl2"),
Entry("SDL", "SDL", "https://www.libsdl.org/"),
Entry("SDL_mixer", "SDL_mixer", "https://github.com/libsdl-org/SDL_mixer"),
Entry("SDL_ttf", "SDL_ttf", "https://github.com/libsdl-org/SDL_ttf"),
Entry("NanoJPEG", "nanojpeg", "https://keyj.emphy.de/nanojpeg/"),
Entry("microui", "microui", "https://github.com/rxi/microui"),
Entry("FreeType", "freetype", "https://freetype.org/"),
Entry("HarfBuzz", "harfbuzz", "https://harfbuzz.github.io/"),
Entry("MotoyaLCedar W3 mono", "MTLc3m", "https://github.com/aosp-mirror/platform_frameworks_base/tree/lollipop-release/data/fonts"),
Entry("IPA明朝", "mincho", "https://moji.or.jp/ipafont/"),
)
@@ -1,22 +0,0 @@
package org.libsdl.app;
import android.hardware.usb.UsbDevice;
interface HIDDevice
{
public int getId();
public int getVendorId();
public int getProductId();
public String getSerialNumber();
public int getVersion();
public String getManufacturerName();
public String getProductName();
public UsbDevice getDevice();
public boolean open();
public int sendFeatureReport(byte[] report);
public int sendOutputReport(byte[] report);
public boolean getFeatureReport(byte[] report);
public void setFrozen(boolean frozen);
public void close();
public void shutdown();
}
@@ -1,650 +0,0 @@
package org.libsdl.app;
import android.content.Context;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothManager;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothGattService;
import android.hardware.usb.UsbDevice;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.os.*;
//import com.android.internal.util.HexDump;
import java.lang.Runnable;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.UUID;
class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDevice {
private static final String TAG = "hidapi";
private HIDDeviceManager mManager;
private BluetoothDevice mDevice;
private int mDeviceId;
private BluetoothGatt mGatt;
private boolean mIsRegistered = false;
private boolean mIsConnected = false;
private boolean mIsChromebook = false;
private boolean mIsReconnecting = false;
private boolean mFrozen = false;
private LinkedList<GattOperation> mOperations;
GattOperation mCurrentOperation = null;
private Handler mHandler;
private static final int TRANSPORT_AUTO = 0;
private static final int TRANSPORT_BREDR = 1;
private static final int TRANSPORT_LE = 2;
private static final int CHROMEBOOK_CONNECTION_CHECK_INTERVAL = 10000;
static public final UUID steamControllerService = UUID.fromString("100F6C32-1735-4313-B402-38567131E5F3");
static public final UUID inputCharacteristic = UUID.fromString("100F6C33-1735-4313-B402-38567131E5F3");
static public final UUID reportCharacteristic = UUID.fromString("100F6C34-1735-4313-B402-38567131E5F3");
static private final byte[] enterValveMode = new byte[] { (byte)0xC0, (byte)0x87, 0x03, 0x08, 0x07, 0x00 };
static class GattOperation {
private enum Operation {
CHR_READ,
CHR_WRITE,
ENABLE_NOTIFICATION
}
Operation mOp;
UUID mUuid;
byte[] mValue;
BluetoothGatt mGatt;
boolean mResult = true;
private GattOperation(BluetoothGatt gatt, GattOperation.Operation operation, UUID uuid) {
mGatt = gatt;
mOp = operation;
mUuid = uuid;
}
private GattOperation(BluetoothGatt gatt, GattOperation.Operation operation, UUID uuid, byte[] value) {
mGatt = gatt;
mOp = operation;
mUuid = uuid;
mValue = value;
}
public void run() {
// This is executed in main thread
BluetoothGattCharacteristic chr;
switch (mOp) {
case CHR_READ:
chr = getCharacteristic(mUuid);
//Log.v(TAG, "Reading characteristic " + chr.getUuid());
if (!mGatt.readCharacteristic(chr)) {
Log.e(TAG, "Unable to read characteristic " + mUuid.toString());
mResult = false;
break;
}
mResult = true;
break;
case CHR_WRITE:
chr = getCharacteristic(mUuid);
//Log.v(TAG, "Writing characteristic " + chr.getUuid() + " value=" + HexDump.toHexString(value));
chr.setValue(mValue);
if (!mGatt.writeCharacteristic(chr)) {
Log.e(TAG, "Unable to write characteristic " + mUuid.toString());
mResult = false;
break;
}
mResult = true;
break;
case ENABLE_NOTIFICATION:
chr = getCharacteristic(mUuid);
//Log.v(TAG, "Writing descriptor of " + chr.getUuid());
if (chr != null) {
BluetoothGattDescriptor cccd = chr.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"));
if (cccd != null) {
int properties = chr.getProperties();
byte[] value;
if ((properties & BluetoothGattCharacteristic.PROPERTY_NOTIFY) == BluetoothGattCharacteristic.PROPERTY_NOTIFY) {
value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE;
} else if ((properties & BluetoothGattCharacteristic.PROPERTY_INDICATE) == BluetoothGattCharacteristic.PROPERTY_INDICATE) {
value = BluetoothGattDescriptor.ENABLE_INDICATION_VALUE;
} else {
Log.e(TAG, "Unable to start notifications on input characteristic");
mResult = false;
return;
}
mGatt.setCharacteristicNotification(chr, true);
cccd.setValue(value);
if (!mGatt.writeDescriptor(cccd)) {
Log.e(TAG, "Unable to write descriptor " + mUuid.toString());
mResult = false;
return;
}
mResult = true;
}
}
}
}
public boolean finish() {
return mResult;
}
private BluetoothGattCharacteristic getCharacteristic(UUID uuid) {
BluetoothGattService valveService = mGatt.getService(steamControllerService);
if (valveService == null)
return null;
return valveService.getCharacteristic(uuid);
}
static public GattOperation readCharacteristic(BluetoothGatt gatt, UUID uuid) {
return new GattOperation(gatt, Operation.CHR_READ, uuid);
}
static public GattOperation writeCharacteristic(BluetoothGatt gatt, UUID uuid, byte[] value) {
return new GattOperation(gatt, Operation.CHR_WRITE, uuid, value);
}
static public GattOperation enableNotification(BluetoothGatt gatt, UUID uuid) {
return new GattOperation(gatt, Operation.ENABLE_NOTIFICATION, uuid);
}
}
public HIDDeviceBLESteamController(HIDDeviceManager manager, BluetoothDevice device) {
mManager = manager;
mDevice = device;
mDeviceId = mManager.getDeviceIDForIdentifier(getIdentifier());
mIsRegistered = false;
mIsChromebook = mManager.getContext().getPackageManager().hasSystemFeature("org.chromium.arc.device_management");
mOperations = new LinkedList<GattOperation>();
mHandler = new Handler(Looper.getMainLooper());
mGatt = connectGatt();
// final HIDDeviceBLESteamController finalThis = this;
// mHandler.postDelayed(new Runnable() {
// @Override
// public void run() {
// finalThis.checkConnectionForChromebookIssue();
// }
// }, CHROMEBOOK_CONNECTION_CHECK_INTERVAL);
}
public String getIdentifier() {
return String.format("SteamController.%s", mDevice.getAddress());
}
public BluetoothGatt getGatt() {
return mGatt;
}
// Because on Chromebooks we show up as a dual-mode device, it will attempt to connect TRANSPORT_AUTO, which will use TRANSPORT_BREDR instead
// of TRANSPORT_LE. Let's force ourselves to connect low energy.
private BluetoothGatt connectGatt(boolean managed) {
if (Build.VERSION.SDK_INT >= 23 /* Android 6.0 (M) */) {
try {
return mDevice.connectGatt(mManager.getContext(), managed, this, TRANSPORT_LE);
} catch (Exception e) {
return mDevice.connectGatt(mManager.getContext(), managed, this);
}
} else {
return mDevice.connectGatt(mManager.getContext(), managed, this);
}
}
private BluetoothGatt connectGatt() {
return connectGatt(false);
}
protected int getConnectionState() {
Context context = mManager.getContext();
if (context == null) {
// We are lacking any context to get our Bluetooth information. We'll just assume disconnected.
return BluetoothProfile.STATE_DISCONNECTED;
}
BluetoothManager btManager = (BluetoothManager)context.getSystemService(Context.BLUETOOTH_SERVICE);
if (btManager == null) {
// This device doesn't support Bluetooth. We should never be here, because how did
// we instantiate a device to start with?
return BluetoothProfile.STATE_DISCONNECTED;
}
return btManager.getConnectionState(mDevice, BluetoothProfile.GATT);
}
public void reconnect() {
if (getConnectionState() != BluetoothProfile.STATE_CONNECTED) {
mGatt.disconnect();
mGatt = connectGatt();
}
}
protected void checkConnectionForChromebookIssue() {
if (!mIsChromebook) {
// We only do this on Chromebooks, because otherwise it's really annoying to just attempt
// over and over.
return;
}
int connectionState = getConnectionState();
switch (connectionState) {
case BluetoothProfile.STATE_CONNECTED:
if (!mIsConnected) {
// We are in the Bad Chromebook Place. We can force a disconnect
// to try to recover.
Log.v(TAG, "Chromebook: We are in a very bad state; the controller shows as connected in the underlying Bluetooth layer, but we never received a callback. Forcing a reconnect.");
mIsReconnecting = true;
mGatt.disconnect();
mGatt = connectGatt(false);
break;
}
else if (!isRegistered()) {
if (mGatt.getServices().size() > 0) {
Log.v(TAG, "Chromebook: We are connected to a controller, but never got our registration. Trying to recover.");
probeService(this);
}
else {
Log.v(TAG, "Chromebook: We are connected to a controller, but never discovered services. Trying to recover.");
mIsReconnecting = true;
mGatt.disconnect();
mGatt = connectGatt(false);
break;
}
}
else {
Log.v(TAG, "Chromebook: We are connected, and registered. Everything's good!");
return;
}
break;
case BluetoothProfile.STATE_DISCONNECTED:
Log.v(TAG, "Chromebook: We have either been disconnected, or the Chromebook BtGatt.ContextMap bug has bitten us. Attempting a disconnect/reconnect, but we may not be able to recover.");
mIsReconnecting = true;
mGatt.disconnect();
mGatt = connectGatt(false);
break;
case BluetoothProfile.STATE_CONNECTING:
Log.v(TAG, "Chromebook: We're still trying to connect. Waiting a bit longer.");
break;
}
final HIDDeviceBLESteamController finalThis = this;
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
finalThis.checkConnectionForChromebookIssue();
}
}, CHROMEBOOK_CONNECTION_CHECK_INTERVAL);
}
private boolean isRegistered() {
return mIsRegistered;
}
private void setRegistered() {
mIsRegistered = true;
}
private boolean probeService(HIDDeviceBLESteamController controller) {
if (isRegistered()) {
return true;
}
if (!mIsConnected) {
return false;
}
Log.v(TAG, "probeService controller=" + controller);
for (BluetoothGattService service : mGatt.getServices()) {
if (service.getUuid().equals(steamControllerService)) {
Log.v(TAG, "Found Valve steam controller service " + service.getUuid());
for (BluetoothGattCharacteristic chr : service.getCharacteristics()) {
if (chr.getUuid().equals(inputCharacteristic)) {
Log.v(TAG, "Found input characteristic");
// Start notifications
BluetoothGattDescriptor cccd = chr.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"));
if (cccd != null) {
enableNotification(chr.getUuid());
}
}
}
return true;
}
}
if ((mGatt.getServices().size() == 0) && mIsChromebook && !mIsReconnecting) {
Log.e(TAG, "Chromebook: Discovered services were empty; this almost certainly means the BtGatt.ContextMap bug has bitten us.");
mIsConnected = false;
mIsReconnecting = true;
mGatt.disconnect();
mGatt = connectGatt(false);
}
return false;
}
//////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////
private void finishCurrentGattOperation() {
GattOperation op = null;
synchronized (mOperations) {
if (mCurrentOperation != null) {
op = mCurrentOperation;
mCurrentOperation = null;
}
}
if (op != null) {
boolean result = op.finish(); // TODO: Maybe in main thread as well?
// Our operation failed, let's add it back to the beginning of our queue.
if (!result) {
mOperations.addFirst(op);
}
}
executeNextGattOperation();
}
private void executeNextGattOperation() {
synchronized (mOperations) {
if (mCurrentOperation != null)
return;
if (mOperations.isEmpty())
return;
mCurrentOperation = mOperations.removeFirst();
}
// Run in main thread
mHandler.post(new Runnable() {
@Override
public void run() {
synchronized (mOperations) {
if (mCurrentOperation == null) {
Log.e(TAG, "Current operation null in executor?");
return;
}
mCurrentOperation.run();
// now wait for the GATT callback and when it comes, finish this operation
}
}
});
}
private void queueGattOperation(GattOperation op) {
synchronized (mOperations) {
mOperations.add(op);
}
executeNextGattOperation();
}
private void enableNotification(UUID chrUuid) {
GattOperation op = HIDDeviceBLESteamController.GattOperation.enableNotification(mGatt, chrUuid);
queueGattOperation(op);
}
public void writeCharacteristic(UUID uuid, byte[] value) {
GattOperation op = HIDDeviceBLESteamController.GattOperation.writeCharacteristic(mGatt, uuid, value);
queueGattOperation(op);
}
public void readCharacteristic(UUID uuid) {
GattOperation op = HIDDeviceBLESteamController.GattOperation.readCharacteristic(mGatt, uuid);
queueGattOperation(op);
}
//////////////////////////////////////////////////////////////////////////////////////////////////////
////////////// BluetoothGattCallback overridden methods
//////////////////////////////////////////////////////////////////////////////////////////////////////
public void onConnectionStateChange(BluetoothGatt g, int status, int newState) {
//Log.v(TAG, "onConnectionStateChange status=" + status + " newState=" + newState);
mIsReconnecting = false;
if (newState == 2) {
mIsConnected = true;
// Run directly, without GattOperation
if (!isRegistered()) {
mHandler.post(new Runnable() {
@Override
public void run() {
mGatt.discoverServices();
}
});
}
}
else if (newState == 0) {
mIsConnected = false;
}
// Disconnection is handled in SteamLink using the ACTION_ACL_DISCONNECTED Intent.
}
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
//Log.v(TAG, "onServicesDiscovered status=" + status);
if (status == 0) {
if (gatt.getServices().size() == 0) {
Log.v(TAG, "onServicesDiscovered returned zero services; something has gone horribly wrong down in Android's Bluetooth stack.");
mIsReconnecting = true;
mIsConnected = false;
gatt.disconnect();
mGatt = connectGatt(false);
}
else {
probeService(this);
}
}
}
public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
//Log.v(TAG, "onCharacteristicRead status=" + status + " uuid=" + characteristic.getUuid());
if (characteristic.getUuid().equals(reportCharacteristic) && !mFrozen) {
mManager.HIDDeviceFeatureReport(getId(), characteristic.getValue());
}
finishCurrentGattOperation();
}
public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
//Log.v(TAG, "onCharacteristicWrite status=" + status + " uuid=" + characteristic.getUuid());
if (characteristic.getUuid().equals(reportCharacteristic)) {
// Only register controller with the native side once it has been fully configured
if (!isRegistered()) {
Log.v(TAG, "Registering Steam Controller with ID: " + getId());
mManager.HIDDeviceConnected(getId(), getIdentifier(), getVendorId(), getProductId(), getSerialNumber(), getVersion(), getManufacturerName(), getProductName(), 0, 0, 0, 0);
setRegistered();
}
}
finishCurrentGattOperation();
}
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
// Enable this for verbose logging of controller input reports
//Log.v(TAG, "onCharacteristicChanged uuid=" + characteristic.getUuid() + " data=" + HexDump.dumpHexString(characteristic.getValue()));
if (characteristic.getUuid().equals(inputCharacteristic) && !mFrozen) {
mManager.HIDDeviceInputReport(getId(), characteristic.getValue());
}
}
public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
//Log.v(TAG, "onDescriptorRead status=" + status);
}
public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
BluetoothGattCharacteristic chr = descriptor.getCharacteristic();
//Log.v(TAG, "onDescriptorWrite status=" + status + " uuid=" + chr.getUuid() + " descriptor=" + descriptor.getUuid());
if (chr.getUuid().equals(inputCharacteristic)) {
boolean hasWrittenInputDescriptor = true;
BluetoothGattCharacteristic reportChr = chr.getService().getCharacteristic(reportCharacteristic);
if (reportChr != null) {
Log.v(TAG, "Writing report characteristic to enter valve mode");
reportChr.setValue(enterValveMode);
gatt.writeCharacteristic(reportChr);
}
}
finishCurrentGattOperation();
}
public void onReliableWriteCompleted(BluetoothGatt gatt, int status) {
//Log.v(TAG, "onReliableWriteCompleted status=" + status);
}
public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status) {
//Log.v(TAG, "onReadRemoteRssi status=" + status);
}
public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
//Log.v(TAG, "onMtuChanged status=" + status);
}
//////////////////////////////////////////////////////////////////////////////////////////////////////
//////// Public API
//////////////////////////////////////////////////////////////////////////////////////////////////////
@Override
public int getId() {
return mDeviceId;
}
@Override
public int getVendorId() {
// Valve Corporation
final int VALVE_USB_VID = 0x28DE;
return VALVE_USB_VID;
}
@Override
public int getProductId() {
// We don't have an easy way to query from the Bluetooth device, but we know what it is
final int D0G_BLE2_PID = 0x1106;
return D0G_BLE2_PID;
}
@Override
public String getSerialNumber() {
// This will be read later via feature report by Steam
return "12345";
}
@Override
public int getVersion() {
return 0;
}
@Override
public String getManufacturerName() {
return "Valve Corporation";
}
@Override
public String getProductName() {
return "Steam Controller";
}
@Override
public UsbDevice getDevice() {
return null;
}
@Override
public boolean open() {
return true;
}
@Override
public int sendFeatureReport(byte[] report) {
if (!isRegistered()) {
Log.e(TAG, "Attempted sendFeatureReport before Steam Controller is registered!");
if (mIsConnected) {
probeService(this);
}
return -1;
}
// We need to skip the first byte, as that doesn't go over the air
byte[] actual_report = Arrays.copyOfRange(report, 1, report.length - 1);
//Log.v(TAG, "sendFeatureReport " + HexDump.dumpHexString(actual_report));
writeCharacteristic(reportCharacteristic, actual_report);
return report.length;
}
@Override
public int sendOutputReport(byte[] report) {
if (!isRegistered()) {
Log.e(TAG, "Attempted sendOutputReport before Steam Controller is registered!");
if (mIsConnected) {
probeService(this);
}
return -1;
}
//Log.v(TAG, "sendFeatureReport " + HexDump.dumpHexString(report));
writeCharacteristic(reportCharacteristic, report);
return report.length;
}
@Override
public boolean getFeatureReport(byte[] report) {
if (!isRegistered()) {
Log.e(TAG, "Attempted getFeatureReport before Steam Controller is registered!");
if (mIsConnected) {
probeService(this);
}
return false;
}
//Log.v(TAG, "getFeatureReport");
readCharacteristic(reportCharacteristic);
return true;
}
@Override
public void close() {
}
@Override
public void setFrozen(boolean frozen) {
mFrozen = frozen;
}
@Override
public void shutdown() {
close();
BluetoothGatt g = mGatt;
if (g != null) {
g.disconnect();
g.close();
mGatt = null;
}
mManager = null;
mIsRegistered = false;
mIsConnected = false;
mOperations.clear();
}
}
@@ -1,698 +0,0 @@
package org.libsdl.app;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.PendingIntent;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothManager;
import android.bluetooth.BluetoothProfile;
import android.os.Build;
import android.util.Log;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.hardware.usb.*;
import android.os.Handler;
import android.os.Looper;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
public class HIDDeviceManager {
private static final String TAG = "hidapi";
private static final String ACTION_USB_PERMISSION = "org.libsdl.app.USB_PERMISSION";
private static HIDDeviceManager sManager;
private static int sManagerRefCount = 0;
public static HIDDeviceManager acquire(Context context) {
if (sManagerRefCount == 0) {
sManager = new HIDDeviceManager(context);
}
++sManagerRefCount;
return sManager;
}
public static void release(HIDDeviceManager manager) {
if (manager == sManager) {
--sManagerRefCount;
if (sManagerRefCount == 0) {
sManager.close();
sManager = null;
}
}
}
private Context mContext;
private HashMap<Integer, HIDDevice> mDevicesById = new HashMap<Integer, HIDDevice>();
private HashMap<BluetoothDevice, HIDDeviceBLESteamController> mBluetoothDevices = new HashMap<BluetoothDevice, HIDDeviceBLESteamController>();
private int mNextDeviceId = 0;
private SharedPreferences mSharedPreferences = null;
private boolean mIsChromebook = false;
private UsbManager mUsbManager;
private Handler mHandler;
private BluetoothManager mBluetoothManager;
private List<BluetoothDevice> mLastBluetoothDevices;
private final BroadcastReceiver mUsbBroadcast = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (action.equals(UsbManager.ACTION_USB_DEVICE_ATTACHED)) {
UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
handleUsbDeviceAttached(usbDevice);
} else if (action.equals(UsbManager.ACTION_USB_DEVICE_DETACHED)) {
UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
handleUsbDeviceDetached(usbDevice);
} else if (action.equals(HIDDeviceManager.ACTION_USB_PERMISSION)) {
UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
handleUsbDevicePermission(usbDevice, intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false));
}
}
};
private final BroadcastReceiver mBluetoothBroadcast = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
// Bluetooth device was connected. If it was a Steam Controller, handle it
if (action.equals(BluetoothDevice.ACTION_ACL_CONNECTED)) {
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
Log.d(TAG, "Bluetooth device connected: " + device);
if (isSteamController(device)) {
connectBluetoothDevice(device);
}
}
// Bluetooth device was disconnected, remove from controller manager (if any)
if (action.equals(BluetoothDevice.ACTION_ACL_DISCONNECTED)) {
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
Log.d(TAG, "Bluetooth device disconnected: " + device);
disconnectBluetoothDevice(device);
}
}
};
private HIDDeviceManager(final Context context) {
mContext = context;
HIDDeviceRegisterCallback();
mSharedPreferences = mContext.getSharedPreferences("hidapi", Context.MODE_PRIVATE);
mIsChromebook = mContext.getPackageManager().hasSystemFeature("org.chromium.arc.device_management");
// if (shouldClear) {
// SharedPreferences.Editor spedit = mSharedPreferences.edit();
// spedit.clear();
// spedit.commit();
// }
// else
{
mNextDeviceId = mSharedPreferences.getInt("next_device_id", 0);
}
}
public Context getContext() {
return mContext;
}
public int getDeviceIDForIdentifier(String identifier) {
SharedPreferences.Editor spedit = mSharedPreferences.edit();
int result = mSharedPreferences.getInt(identifier, 0);
if (result == 0) {
result = mNextDeviceId++;
spedit.putInt("next_device_id", mNextDeviceId);
}
spedit.putInt(identifier, result);
spedit.commit();
return result;
}
private void initializeUSB() {
mUsbManager = (UsbManager)mContext.getSystemService(Context.USB_SERVICE);
if (mUsbManager == null) {
return;
}
/*
// Logging
for (UsbDevice device : mUsbManager.getDeviceList().values()) {
Log.i(TAG,"Path: " + device.getDeviceName());
Log.i(TAG,"Manufacturer: " + device.getManufacturerName());
Log.i(TAG,"Product: " + device.getProductName());
Log.i(TAG,"ID: " + device.getDeviceId());
Log.i(TAG,"Class: " + device.getDeviceClass());
Log.i(TAG,"Protocol: " + device.getDeviceProtocol());
Log.i(TAG,"Vendor ID " + device.getVendorId());
Log.i(TAG,"Product ID: " + device.getProductId());
Log.i(TAG,"Interface count: " + device.getInterfaceCount());
Log.i(TAG,"---------------------------------------");
// Get interface details
for (int index = 0; index < device.getInterfaceCount(); index++) {
UsbInterface mUsbInterface = device.getInterface(index);
Log.i(TAG," ***** *****");
Log.i(TAG," Interface index: " + index);
Log.i(TAG," Interface ID: " + mUsbInterface.getId());
Log.i(TAG," Interface class: " + mUsbInterface.getInterfaceClass());
Log.i(TAG," Interface subclass: " + mUsbInterface.getInterfaceSubclass());
Log.i(TAG," Interface protocol: " + mUsbInterface.getInterfaceProtocol());
Log.i(TAG," Endpoint count: " + mUsbInterface.getEndpointCount());
// Get endpoint details
for (int epi = 0; epi < mUsbInterface.getEndpointCount(); epi++)
{
UsbEndpoint mEndpoint = mUsbInterface.getEndpoint(epi);
Log.i(TAG," ++++ ++++ ++++");
Log.i(TAG," Endpoint index: " + epi);
Log.i(TAG," Attributes: " + mEndpoint.getAttributes());
Log.i(TAG," Direction: " + mEndpoint.getDirection());
Log.i(TAG," Number: " + mEndpoint.getEndpointNumber());
Log.i(TAG," Interval: " + mEndpoint.getInterval());
Log.i(TAG," Packet size: " + mEndpoint.getMaxPacketSize());
Log.i(TAG," Type: " + mEndpoint.getType());
}
}
}
Log.i(TAG," No more devices connected.");
*/
// Register for USB broadcasts and permission completions
IntentFilter filter = new IntentFilter();
filter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED);
filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED);
filter.addAction(HIDDeviceManager.ACTION_USB_PERMISSION);
mContext.registerReceiver(mUsbBroadcast, filter);
for (UsbDevice usbDevice : mUsbManager.getDeviceList().values()) {
handleUsbDeviceAttached(usbDevice);
}
}
UsbManager getUSBManager() {
return mUsbManager;
}
private void shutdownUSB() {
try {
mContext.unregisterReceiver(mUsbBroadcast);
} catch (Exception e) {
// We may not have registered, that's okay
}
}
private boolean isHIDDeviceInterface(UsbDevice usbDevice, UsbInterface usbInterface) {
if (usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_HID) {
return true;
}
if (isXbox360Controller(usbDevice, usbInterface) || isXboxOneController(usbDevice, usbInterface)) {
return true;
}
return false;
}
private boolean isXbox360Controller(UsbDevice usbDevice, UsbInterface usbInterface) {
final int XB360_IFACE_SUBCLASS = 93;
final int XB360_IFACE_PROTOCOL = 1; // Wired
final int XB360W_IFACE_PROTOCOL = 129; // Wireless
final int[] SUPPORTED_VENDORS = {
0x0079, // GPD Win 2
0x044f, // Thrustmaster
0x045e, // Microsoft
0x046d, // Logitech
0x056e, // Elecom
0x06a3, // Saitek
0x0738, // Mad Catz
0x07ff, // Mad Catz
0x0e6f, // PDP
0x0f0d, // Hori
0x1038, // SteelSeries
0x11c9, // Nacon
0x12ab, // Unknown
0x1430, // RedOctane
0x146b, // BigBen
0x1532, // Razer Sabertooth
0x15e4, // Numark
0x162e, // Joytech
0x1689, // Razer Onza
0x1949, // Lab126, Inc.
0x1bad, // Harmonix
0x20d6, // PowerA
0x24c6, // PowerA
0x2c22, // Qanba
0x2dc8, // 8BitDo
0x9886, // ASTRO Gaming
};
if (usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_VENDOR_SPEC &&
usbInterface.getInterfaceSubclass() == XB360_IFACE_SUBCLASS &&
(usbInterface.getInterfaceProtocol() == XB360_IFACE_PROTOCOL ||
usbInterface.getInterfaceProtocol() == XB360W_IFACE_PROTOCOL)) {
int vendor_id = usbDevice.getVendorId();
for (int supportedVid : SUPPORTED_VENDORS) {
if (vendor_id == supportedVid) {
return true;
}
}
}
return false;
}
private boolean isXboxOneController(UsbDevice usbDevice, UsbInterface usbInterface) {
final int XB1_IFACE_SUBCLASS = 71;
final int XB1_IFACE_PROTOCOL = 208;
final int[] SUPPORTED_VENDORS = {
0x03f0, // HP
0x044f, // Thrustmaster
0x045e, // Microsoft
0x0738, // Mad Catz
0x0b05, // ASUS
0x0e6f, // PDP
0x0f0d, // Hori
0x10f5, // Turtle Beach
0x1532, // Razer Wildcat
0x20d6, // PowerA
0x24c6, // PowerA
0x2dc8, // 8BitDo
0x2e24, // Hyperkin
0x3537, // GameSir
};
if (usbInterface.getId() == 0 &&
usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_VENDOR_SPEC &&
usbInterface.getInterfaceSubclass() == XB1_IFACE_SUBCLASS &&
usbInterface.getInterfaceProtocol() == XB1_IFACE_PROTOCOL) {
int vendor_id = usbDevice.getVendorId();
for (int supportedVid : SUPPORTED_VENDORS) {
if (vendor_id == supportedVid) {
return true;
}
}
}
return false;
}
private void handleUsbDeviceAttached(UsbDevice usbDevice) {
connectHIDDeviceUSB(usbDevice);
}
private void handleUsbDeviceDetached(UsbDevice usbDevice) {
List<Integer> devices = new ArrayList<Integer>();
for (HIDDevice device : mDevicesById.values()) {
if (usbDevice.equals(device.getDevice())) {
devices.add(device.getId());
}
}
for (int id : devices) {
HIDDevice device = mDevicesById.get(id);
mDevicesById.remove(id);
device.shutdown();
HIDDeviceDisconnected(id);
}
}
private void handleUsbDevicePermission(UsbDevice usbDevice, boolean permission_granted) {
for (HIDDevice device : mDevicesById.values()) {
if (usbDevice.equals(device.getDevice())) {
boolean opened = false;
if (permission_granted) {
opened = device.open();
}
HIDDeviceOpenResult(device.getId(), opened);
}
}
}
private void connectHIDDeviceUSB(UsbDevice usbDevice) {
synchronized (this) {
int interface_mask = 0;
for (int interface_index = 0; interface_index < usbDevice.getInterfaceCount(); interface_index++) {
UsbInterface usbInterface = usbDevice.getInterface(interface_index);
if (isHIDDeviceInterface(usbDevice, usbInterface)) {
// Check to see if we've already added this interface
// This happens with the Xbox Series X controller which has a duplicate interface 0, which is inactive
int interface_id = usbInterface.getId();
if ((interface_mask & (1 << interface_id)) != 0) {
continue;
}
interface_mask |= (1 << interface_id);
HIDDeviceUSB device = new HIDDeviceUSB(this, usbDevice, interface_index);
int id = device.getId();
mDevicesById.put(id, device);
HIDDeviceConnected(id, device.getIdentifier(), device.getVendorId(), device.getProductId(), device.getSerialNumber(), device.getVersion(), device.getManufacturerName(), device.getProductName(), usbInterface.getId(), usbInterface.getInterfaceClass(), usbInterface.getInterfaceSubclass(), usbInterface.getInterfaceProtocol());
}
}
}
}
private void initializeBluetooth() {
Log.d(TAG, "Initializing Bluetooth");
if (Build.VERSION.SDK_INT >= 31 /* Android 12 */ &&
mContext.getPackageManager().checkPermission(android.Manifest.permission.BLUETOOTH_CONNECT, mContext.getPackageName()) != PackageManager.PERMISSION_GRANTED) {
Log.d(TAG, "Couldn't initialize Bluetooth, missing android.permission.BLUETOOTH_CONNECT");
return;
}
if (Build.VERSION.SDK_INT <= 30 /* Android 11.0 (R) */ &&
mContext.getPackageManager().checkPermission(android.Manifest.permission.BLUETOOTH, mContext.getPackageName()) != PackageManager.PERMISSION_GRANTED) {
Log.d(TAG, "Couldn't initialize Bluetooth, missing android.permission.BLUETOOTH");
return;
}
if (!mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE) || (Build.VERSION.SDK_INT < 18 /* Android 4.3 (JELLY_BEAN_MR2) */)) {
Log.d(TAG, "Couldn't initialize Bluetooth, this version of Android does not support Bluetooth LE");
return;
}
// Find bonded bluetooth controllers and create SteamControllers for them
mBluetoothManager = (BluetoothManager)mContext.getSystemService(Context.BLUETOOTH_SERVICE);
if (mBluetoothManager == null) {
// This device doesn't support Bluetooth.
return;
}
BluetoothAdapter btAdapter = mBluetoothManager.getAdapter();
if (btAdapter == null) {
// This device has Bluetooth support in the codebase, but has no available adapters.
return;
}
// Get our bonded devices.
for (BluetoothDevice device : btAdapter.getBondedDevices()) {
Log.d(TAG, "Bluetooth device available: " + device);
if (isSteamController(device)) {
connectBluetoothDevice(device);
}
}
// NOTE: These don't work on Chromebooks, to my undying dismay.
IntentFilter filter = new IntentFilter();
filter.addAction(BluetoothDevice.ACTION_ACL_CONNECTED);
filter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED);
mContext.registerReceiver(mBluetoothBroadcast, filter);
if (mIsChromebook) {
mHandler = new Handler(Looper.getMainLooper());
mLastBluetoothDevices = new ArrayList<BluetoothDevice>();
// final HIDDeviceManager finalThis = this;
// mHandler.postDelayed(new Runnable() {
// @Override
// public void run() {
// finalThis.chromebookConnectionHandler();
// }
// }, 5000);
}
}
private void shutdownBluetooth() {
try {
mContext.unregisterReceiver(mBluetoothBroadcast);
} catch (Exception e) {
// We may not have registered, that's okay
}
}
// Chromebooks do not pass along ACTION_ACL_CONNECTED / ACTION_ACL_DISCONNECTED properly.
// This function provides a sort of dummy version of that, watching for changes in the
// connected devices and attempting to add controllers as things change.
public void chromebookConnectionHandler() {
if (!mIsChromebook) {
return;
}
ArrayList<BluetoothDevice> disconnected = new ArrayList<BluetoothDevice>();
ArrayList<BluetoothDevice> connected = new ArrayList<BluetoothDevice>();
List<BluetoothDevice> currentConnected = mBluetoothManager.getConnectedDevices(BluetoothProfile.GATT);
for (BluetoothDevice bluetoothDevice : currentConnected) {
if (!mLastBluetoothDevices.contains(bluetoothDevice)) {
connected.add(bluetoothDevice);
}
}
for (BluetoothDevice bluetoothDevice : mLastBluetoothDevices) {
if (!currentConnected.contains(bluetoothDevice)) {
disconnected.add(bluetoothDevice);
}
}
mLastBluetoothDevices = currentConnected;
for (BluetoothDevice bluetoothDevice : disconnected) {
disconnectBluetoothDevice(bluetoothDevice);
}
for (BluetoothDevice bluetoothDevice : connected) {
connectBluetoothDevice(bluetoothDevice);
}
final HIDDeviceManager finalThis = this;
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
finalThis.chromebookConnectionHandler();
}
}, 10000);
}
public boolean connectBluetoothDevice(BluetoothDevice bluetoothDevice) {
Log.v(TAG, "connectBluetoothDevice device=" + bluetoothDevice);
synchronized (this) {
if (mBluetoothDevices.containsKey(bluetoothDevice)) {
Log.v(TAG, "Steam controller with address " + bluetoothDevice + " already exists, attempting reconnect");
HIDDeviceBLESteamController device = mBluetoothDevices.get(bluetoothDevice);
device.reconnect();
return false;
}
HIDDeviceBLESteamController device = new HIDDeviceBLESteamController(this, bluetoothDevice);
int id = device.getId();
mBluetoothDevices.put(bluetoothDevice, device);
mDevicesById.put(id, device);
// The Steam Controller will mark itself connected once initialization is complete
}
return true;
}
public void disconnectBluetoothDevice(BluetoothDevice bluetoothDevice) {
synchronized (this) {
HIDDeviceBLESteamController device = mBluetoothDevices.get(bluetoothDevice);
if (device == null)
return;
int id = device.getId();
mBluetoothDevices.remove(bluetoothDevice);
mDevicesById.remove(id);
device.shutdown();
HIDDeviceDisconnected(id);
}
}
public boolean isSteamController(BluetoothDevice bluetoothDevice) {
// Sanity check. If you pass in a null device, by definition it is never a Steam Controller.
if (bluetoothDevice == null) {
return false;
}
// If the device has no local name, we really don't want to try an equality check against it.
if (bluetoothDevice.getName() == null) {
return false;
}
return bluetoothDevice.getName().equals("SteamController") && ((bluetoothDevice.getType() & BluetoothDevice.DEVICE_TYPE_LE) != 0);
}
private void close() {
shutdownUSB();
shutdownBluetooth();
synchronized (this) {
for (HIDDevice device : mDevicesById.values()) {
device.shutdown();
}
mDevicesById.clear();
mBluetoothDevices.clear();
HIDDeviceReleaseCallback();
}
}
public void setFrozen(boolean frozen) {
synchronized (this) {
for (HIDDevice device : mDevicesById.values()) {
device.setFrozen(frozen);
}
}
}
//////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////
private HIDDevice getDevice(int id) {
synchronized (this) {
HIDDevice result = mDevicesById.get(id);
if (result == null) {
Log.v(TAG, "No device for id: " + id);
Log.v(TAG, "Available devices: " + mDevicesById.keySet());
}
return result;
}
}
//////////////////////////////////////////////////////////////////////////////////////////////////////
////////// JNI interface functions
//////////////////////////////////////////////////////////////////////////////////////////////////////
public boolean initialize(boolean usb, boolean bluetooth) {
Log.v(TAG, "initialize(" + usb + ", " + bluetooth + ")");
if (usb) {
initializeUSB();
}
if (bluetooth) {
initializeBluetooth();
}
return true;
}
public boolean openDevice(int deviceID) {
Log.v(TAG, "openDevice deviceID=" + deviceID);
HIDDevice device = getDevice(deviceID);
if (device == null) {
HIDDeviceDisconnected(deviceID);
return false;
}
// Look to see if this is a USB device and we have permission to access it
UsbDevice usbDevice = device.getDevice();
if (usbDevice != null && !mUsbManager.hasPermission(usbDevice)) {
HIDDeviceOpenPending(deviceID);
try {
final int FLAG_MUTABLE = 0x02000000; // PendingIntent.FLAG_MUTABLE, but don't require SDK 31
int flags;
if (Build.VERSION.SDK_INT >= 31 /* Android 12.0 (S) */) {
flags = FLAG_MUTABLE;
} else {
flags = 0;
}
if (Build.VERSION.SDK_INT >= 33 /* Android 14.0 (U) */) {
Intent intent = new Intent(HIDDeviceManager.ACTION_USB_PERMISSION);
intent.setPackage(mContext.getPackageName());
mUsbManager.requestPermission(usbDevice, PendingIntent.getBroadcast(mContext, 0, intent, flags));
} else {
mUsbManager.requestPermission(usbDevice, PendingIntent.getBroadcast(mContext, 0, new Intent(HIDDeviceManager.ACTION_USB_PERMISSION), flags));
}
} catch (Exception e) {
Log.v(TAG, "Couldn't request permission for USB device " + usbDevice);
HIDDeviceOpenResult(deviceID, false);
}
return false;
}
try {
return device.open();
} catch (Exception e) {
Log.e(TAG, "Got exception: " + Log.getStackTraceString(e));
}
return false;
}
public int sendOutputReport(int deviceID, byte[] report) {
try {
//Log.v(TAG, "sendOutputReport deviceID=" + deviceID + " length=" + report.length);
HIDDevice device;
device = getDevice(deviceID);
if (device == null) {
HIDDeviceDisconnected(deviceID);
return -1;
}
return device.sendOutputReport(report);
} catch (Exception e) {
Log.e(TAG, "Got exception: " + Log.getStackTraceString(e));
}
return -1;
}
public int sendFeatureReport(int deviceID, byte[] report) {
try {
//Log.v(TAG, "sendFeatureReport deviceID=" + deviceID + " length=" + report.length);
HIDDevice device;
device = getDevice(deviceID);
if (device == null) {
HIDDeviceDisconnected(deviceID);
return -1;
}
return device.sendFeatureReport(report);
} catch (Exception e) {
Log.e(TAG, "Got exception: " + Log.getStackTraceString(e));
}
return -1;
}
public boolean getFeatureReport(int deviceID, byte[] report) {
try {
//Log.v(TAG, "getFeatureReport deviceID=" + deviceID);
HIDDevice device;
device = getDevice(deviceID);
if (device == null) {
HIDDeviceDisconnected(deviceID);
return false;
}
return device.getFeatureReport(report);
} catch (Exception e) {
Log.e(TAG, "Got exception: " + Log.getStackTraceString(e));
}
return false;
}
public void closeDevice(int deviceID) {
try {
Log.v(TAG, "closeDevice deviceID=" + deviceID);
HIDDevice device;
device = getDevice(deviceID);
if (device == null) {
HIDDeviceDisconnected(deviceID);
return;
}
device.close();
} catch (Exception e) {
Log.e(TAG, "Got exception: " + Log.getStackTraceString(e));
}
}
//////////////////////////////////////////////////////////////////////////////////////////////////////
/////////////// Native methods
//////////////////////////////////////////////////////////////////////////////////////////////////////
private native void HIDDeviceRegisterCallback();
private native void HIDDeviceReleaseCallback();
native void HIDDeviceConnected(int deviceID, String identifier, int vendorId, int productId, String serial_number, int release_number, String manufacturer_string, String product_string, int interface_number, int interface_class, int interface_subclass, int interface_protocol);
native void HIDDeviceOpenPending(int deviceID);
native void HIDDeviceOpenResult(int deviceID, boolean opened);
native void HIDDeviceDisconnected(int deviceID);
native void HIDDeviceInputReport(int deviceID, byte[] report);
native void HIDDeviceFeatureReport(int deviceID, byte[] report);
}
@@ -1,309 +0,0 @@
package org.libsdl.app;
import android.hardware.usb.*;
import android.os.Build;
import android.util.Log;
import java.util.Arrays;
class HIDDeviceUSB implements HIDDevice {
private static final String TAG = "hidapi";
protected HIDDeviceManager mManager;
protected UsbDevice mDevice;
protected int mInterfaceIndex;
protected int mInterface;
protected int mDeviceId;
protected UsbDeviceConnection mConnection;
protected UsbEndpoint mInputEndpoint;
protected UsbEndpoint mOutputEndpoint;
protected InputThread mInputThread;
protected boolean mRunning;
protected boolean mFrozen;
public HIDDeviceUSB(HIDDeviceManager manager, UsbDevice usbDevice, int interface_index) {
mManager = manager;
mDevice = usbDevice;
mInterfaceIndex = interface_index;
mInterface = mDevice.getInterface(mInterfaceIndex).getId();
mDeviceId = manager.getDeviceIDForIdentifier(getIdentifier());
mRunning = false;
}
public String getIdentifier() {
return String.format("%s/%x/%x/%d", mDevice.getDeviceName(), mDevice.getVendorId(), mDevice.getProductId(), mInterfaceIndex);
}
@Override
public int getId() {
return mDeviceId;
}
@Override
public int getVendorId() {
return mDevice.getVendorId();
}
@Override
public int getProductId() {
return mDevice.getProductId();
}
@Override
public String getSerialNumber() {
String result = null;
if (Build.VERSION.SDK_INT >= 21 /* Android 5.0 (LOLLIPOP) */) {
try {
result = mDevice.getSerialNumber();
}
catch (SecurityException exception) {
//Log.w(TAG, "App permissions mean we cannot get serial number for device " + getDeviceName() + " message: " + exception.getMessage());
}
}
if (result == null) {
result = "";
}
return result;
}
@Override
public int getVersion() {
return 0;
}
@Override
public String getManufacturerName() {
String result = null;
if (Build.VERSION.SDK_INT >= 21 /* Android 5.0 (LOLLIPOP) */) {
result = mDevice.getManufacturerName();
}
if (result == null) {
result = String.format("%x", getVendorId());
}
return result;
}
@Override
public String getProductName() {
String result = null;
if (Build.VERSION.SDK_INT >= 21 /* Android 5.0 (LOLLIPOP) */) {
result = mDevice.getProductName();
}
if (result == null) {
result = String.format("%x", getProductId());
}
return result;
}
@Override
public UsbDevice getDevice() {
return mDevice;
}
public String getDeviceName() {
return getManufacturerName() + " " + getProductName() + "(0x" + String.format("%x", getVendorId()) + "/0x" + String.format("%x", getProductId()) + ")";
}
@Override
public boolean open() {
mConnection = mManager.getUSBManager().openDevice(mDevice);
if (mConnection == null) {
Log.w(TAG, "Unable to open USB device " + getDeviceName());
return false;
}
// Force claim our interface
UsbInterface iface = mDevice.getInterface(mInterfaceIndex);
if (!mConnection.claimInterface(iface, true)) {
Log.w(TAG, "Failed to claim interfaces on USB device " + getDeviceName());
close();
return false;
}
// Find the endpoints
for (int j = 0; j < iface.getEndpointCount(); j++) {
UsbEndpoint endpt = iface.getEndpoint(j);
switch (endpt.getDirection()) {
case UsbConstants.USB_DIR_IN:
if (mInputEndpoint == null) {
mInputEndpoint = endpt;
}
break;
case UsbConstants.USB_DIR_OUT:
if (mOutputEndpoint == null) {
mOutputEndpoint = endpt;
}
break;
}
}
// Make sure the required endpoints were present
if (mInputEndpoint == null || mOutputEndpoint == null) {
Log.w(TAG, "Missing required endpoint on USB device " + getDeviceName());
close();
return false;
}
// Start listening for input
mRunning = true;
mInputThread = new InputThread();
mInputThread.start();
return true;
}
@Override
public int sendFeatureReport(byte[] report) {
int res = -1;
int offset = 0;
int length = report.length;
boolean skipped_report_id = false;
byte report_number = report[0];
if (report_number == 0x0) {
++offset;
--length;
skipped_report_id = true;
}
res = mConnection.controlTransfer(
UsbConstants.USB_TYPE_CLASS | 0x01 /*RECIPIENT_INTERFACE*/ | UsbConstants.USB_DIR_OUT,
0x09/*HID set_report*/,
(3/*HID feature*/ << 8) | report_number,
mInterface,
report, offset, length,
1000/*timeout millis*/);
if (res < 0) {
Log.w(TAG, "sendFeatureReport() returned " + res + " on device " + getDeviceName());
return -1;
}
if (skipped_report_id) {
++length;
}
return length;
}
@Override
public int sendOutputReport(byte[] report) {
int r = mConnection.bulkTransfer(mOutputEndpoint, report, report.length, 1000);
if (r != report.length) {
Log.w(TAG, "sendOutputReport() returned " + r + " on device " + getDeviceName());
}
return r;
}
@Override
public boolean getFeatureReport(byte[] report) {
int res = -1;
int offset = 0;
int length = report.length;
boolean skipped_report_id = false;
byte report_number = report[0];
if (report_number == 0x0) {
/* Offset the return buffer by 1, so that the report ID
will remain in byte 0. */
++offset;
--length;
skipped_report_id = true;
}
res = mConnection.controlTransfer(
UsbConstants.USB_TYPE_CLASS | 0x01 /*RECIPIENT_INTERFACE*/ | UsbConstants.USB_DIR_IN,
0x01/*HID get_report*/,
(3/*HID feature*/ << 8) | report_number,
mInterface,
report, offset, length,
1000/*timeout millis*/);
if (res < 0) {
Log.w(TAG, "getFeatureReport() returned " + res + " on device " + getDeviceName());
return false;
}
if (skipped_report_id) {
++res;
++length;
}
byte[] data;
if (res == length) {
data = report;
} else {
data = Arrays.copyOfRange(report, 0, res);
}
mManager.HIDDeviceFeatureReport(mDeviceId, data);
return true;
}
@Override
public void close() {
mRunning = false;
if (mInputThread != null) {
while (mInputThread.isAlive()) {
mInputThread.interrupt();
try {
mInputThread.join();
} catch (InterruptedException e) {
// Keep trying until we're done
}
}
mInputThread = null;
}
if (mConnection != null) {
UsbInterface iface = mDevice.getInterface(mInterfaceIndex);
mConnection.releaseInterface(iface);
mConnection.close();
mConnection = null;
}
}
@Override
public void shutdown() {
close();
mManager = null;
}
@Override
public void setFrozen(boolean frozen) {
mFrozen = frozen;
}
protected class InputThread extends Thread {
@Override
public void run() {
int packetSize = mInputEndpoint.getMaxPacketSize();
byte[] packet = new byte[packetSize];
while (mRunning) {
int r;
try
{
r = mConnection.bulkTransfer(mInputEndpoint, packet, packetSize, 1000);
}
catch (Exception e)
{
Log.v(TAG, "Exception in UsbDeviceConnection bulktransfer: " + e);
break;
}
if (r < 0) {
// Could be a timeout or an I/O error
}
if (r > 0) {
byte[] data;
if (r == packetSize) {
data = packet;
} else {
data = Arrays.copyOfRange(packet, 0, r);
}
if (!mFrozen) {
mManager.HIDDeviceInputReport(mDeviceId, data);
}
}
}
}
}
}
@@ -1,90 +0,0 @@
package org.libsdl.app;
import android.content.Context;
import java.lang.Class;
import java.lang.reflect.Method;
/**
SDL library initialization
*/
public class SDL {
// This function should be called first and sets up the native code
// so it can call into the Java classes
public static void setupJNI() {
SDLActivity.nativeSetupJNI();
SDLAudioManager.nativeSetupJNI();
SDLControllerManager.nativeSetupJNI();
}
// This function should be called each time the activity is started
public static void initialize() {
setContext(null);
SDLActivity.initialize();
SDLAudioManager.initialize();
SDLControllerManager.initialize();
}
// This function stores the current activity (SDL or not)
public static void setContext(Context context) {
SDLAudioManager.setContext(context);
mContext = context;
}
public static Context getContext() {
return mContext;
}
public static void loadLibrary(String libraryName) throws UnsatisfiedLinkError, SecurityException, NullPointerException {
loadLibrary(libraryName, mContext);
}
public static void loadLibrary(String libraryName, Context context) throws UnsatisfiedLinkError, SecurityException, NullPointerException {
if (libraryName == null) {
throw new NullPointerException("No library name provided.");
}
try {
// Let's see if we have ReLinker available in the project. This is necessary for
// some projects that have huge numbers of local libraries bundled, and thus may
// trip a bug in Android's native library loader which ReLinker works around. (If
// loadLibrary works properly, ReLinker will simply use the normal Android method
// internally.)
//
// To use ReLinker, just add it as a dependency. For more information, see
// https://github.com/KeepSafe/ReLinker for ReLinker's repository.
//
Class<?> relinkClass = context.getClassLoader().loadClass("com.getkeepsafe.relinker.ReLinker");
Class<?> relinkListenerClass = context.getClassLoader().loadClass("com.getkeepsafe.relinker.ReLinker$LoadListener");
Class<?> contextClass = context.getClassLoader().loadClass("android.content.Context");
Class<?> stringClass = context.getClassLoader().loadClass("java.lang.String");
// Get a 'force' instance of the ReLinker, so we can ensure libraries are reinstalled if
// they've changed during updates.
Method forceMethod = relinkClass.getDeclaredMethod("force");
Object relinkInstance = forceMethod.invoke(null);
Class<?> relinkInstanceClass = relinkInstance.getClass();
// Actually load the library!
Method loadMethod = relinkInstanceClass.getDeclaredMethod("loadLibrary", contextClass, stringClass, stringClass, relinkListenerClass);
loadMethod.invoke(relinkInstance, context, libraryName, null, null);
}
catch (final Throwable e) {
// Fall back
try {
System.loadLibrary(libraryName);
}
catch (final UnsatisfiedLinkError ule) {
throw ule;
}
catch (final SecurityException se) {
throw se;
}
}
}
protected static Context mContext;
}
File diff suppressed because it is too large Load Diff
@@ -1,514 +0,0 @@
package org.libsdl.app;
import android.content.Context;
import android.media.AudioDeviceCallback;
import android.media.AudioDeviceInfo;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioRecord;
import android.media.AudioTrack;
import android.media.MediaRecorder;
import android.os.Build;
import android.util.Log;
import java.util.Arrays;
public class SDLAudioManager {
protected static final String TAG = "SDLAudio";
protected static AudioTrack mAudioTrack;
protected static AudioRecord mAudioRecord;
protected static Context mContext;
private static final int[] NO_DEVICES = {};
private static AudioDeviceCallback mAudioDeviceCallback;
public static void initialize() {
mAudioTrack = null;
mAudioRecord = null;
mAudioDeviceCallback = null;
if(Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */)
{
mAudioDeviceCallback = new AudioDeviceCallback() {
@Override
public void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) {
Arrays.stream(addedDevices).forEach(deviceInfo -> addAudioDevice(deviceInfo.isSink(), deviceInfo.getId()));
}
@Override
public void onAudioDevicesRemoved(AudioDeviceInfo[] removedDevices) {
Arrays.stream(removedDevices).forEach(deviceInfo -> removeAudioDevice(deviceInfo.isSink(), deviceInfo.getId()));
}
};
}
}
public static void setContext(Context context) {
mContext = context;
if (context != null) {
registerAudioDeviceCallback();
}
}
public static void release(Context context) {
unregisterAudioDeviceCallback(context);
}
// Audio
protected static String getAudioFormatString(int audioFormat) {
switch (audioFormat) {
case AudioFormat.ENCODING_PCM_8BIT:
return "8-bit";
case AudioFormat.ENCODING_PCM_16BIT:
return "16-bit";
case AudioFormat.ENCODING_PCM_FLOAT:
return "float";
default:
return Integer.toString(audioFormat);
}
}
protected static int[] open(boolean isCapture, int sampleRate, int audioFormat, int desiredChannels, int desiredFrames, int deviceId) {
int channelConfig;
int sampleSize;
int frameSize;
Log.v(TAG, "Opening " + (isCapture ? "capture" : "playback") + ", requested " + desiredFrames + " frames of " + desiredChannels + " channel " + getAudioFormatString(audioFormat) + " audio at " + sampleRate + " Hz");
/* On older devices let's use known good settings */
if (Build.VERSION.SDK_INT < 21 /* Android 5.0 (LOLLIPOP) */) {
if (desiredChannels > 2) {
desiredChannels = 2;
}
}
/* AudioTrack has sample rate limitation of 48000 (fixed in 5.0.2) */
if (Build.VERSION.SDK_INT < 22 /* Android 5.1 (LOLLIPOP_MR1) */) {
if (sampleRate < 8000) {
sampleRate = 8000;
} else if (sampleRate > 48000) {
sampleRate = 48000;
}
}
if (audioFormat == AudioFormat.ENCODING_PCM_FLOAT) {
int minSDKVersion = (isCapture ? 23 /* Android 6.0 (M) */ : 21 /* Android 5.0 (LOLLIPOP) */);
if (Build.VERSION.SDK_INT < minSDKVersion) {
audioFormat = AudioFormat.ENCODING_PCM_16BIT;
}
}
switch (audioFormat)
{
case AudioFormat.ENCODING_PCM_8BIT:
sampleSize = 1;
break;
case AudioFormat.ENCODING_PCM_16BIT:
sampleSize = 2;
break;
case AudioFormat.ENCODING_PCM_FLOAT:
sampleSize = 4;
break;
default:
Log.v(TAG, "Requested format " + audioFormat + ", getting ENCODING_PCM_16BIT");
audioFormat = AudioFormat.ENCODING_PCM_16BIT;
sampleSize = 2;
break;
}
if (isCapture) {
switch (desiredChannels) {
case 1:
channelConfig = AudioFormat.CHANNEL_IN_MONO;
break;
case 2:
channelConfig = AudioFormat.CHANNEL_IN_STEREO;
break;
default:
Log.v(TAG, "Requested " + desiredChannels + " channels, getting stereo");
desiredChannels = 2;
channelConfig = AudioFormat.CHANNEL_IN_STEREO;
break;
}
} else {
switch (desiredChannels) {
case 1:
channelConfig = AudioFormat.CHANNEL_OUT_MONO;
break;
case 2:
channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
break;
case 3:
channelConfig = AudioFormat.CHANNEL_OUT_STEREO | AudioFormat.CHANNEL_OUT_FRONT_CENTER;
break;
case 4:
channelConfig = AudioFormat.CHANNEL_OUT_QUAD;
break;
case 5:
channelConfig = AudioFormat.CHANNEL_OUT_QUAD | AudioFormat.CHANNEL_OUT_FRONT_CENTER;
break;
case 6:
channelConfig = AudioFormat.CHANNEL_OUT_5POINT1;
break;
case 7:
channelConfig = AudioFormat.CHANNEL_OUT_5POINT1 | AudioFormat.CHANNEL_OUT_BACK_CENTER;
break;
case 8:
if (Build.VERSION.SDK_INT >= 23 /* Android 6.0 (M) */) {
channelConfig = AudioFormat.CHANNEL_OUT_7POINT1_SURROUND;
} else {
Log.v(TAG, "Requested " + desiredChannels + " channels, getting 5.1 surround");
desiredChannels = 6;
channelConfig = AudioFormat.CHANNEL_OUT_5POINT1;
}
break;
default:
Log.v(TAG, "Requested " + desiredChannels + " channels, getting stereo");
desiredChannels = 2;
channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
break;
}
/*
Log.v(TAG, "Speaker configuration (and order of channels):");
if ((channelConfig & 0x00000004) != 0) {
Log.v(TAG, " CHANNEL_OUT_FRONT_LEFT");
}
if ((channelConfig & 0x00000008) != 0) {
Log.v(TAG, " CHANNEL_OUT_FRONT_RIGHT");
}
if ((channelConfig & 0x00000010) != 0) {
Log.v(TAG, " CHANNEL_OUT_FRONT_CENTER");
}
if ((channelConfig & 0x00000020) != 0) {
Log.v(TAG, " CHANNEL_OUT_LOW_FREQUENCY");
}
if ((channelConfig & 0x00000040) != 0) {
Log.v(TAG, " CHANNEL_OUT_BACK_LEFT");
}
if ((channelConfig & 0x00000080) != 0) {
Log.v(TAG, " CHANNEL_OUT_BACK_RIGHT");
}
if ((channelConfig & 0x00000100) != 0) {
Log.v(TAG, " CHANNEL_OUT_FRONT_LEFT_OF_CENTER");
}
if ((channelConfig & 0x00000200) != 0) {
Log.v(TAG, " CHANNEL_OUT_FRONT_RIGHT_OF_CENTER");
}
if ((channelConfig & 0x00000400) != 0) {
Log.v(TAG, " CHANNEL_OUT_BACK_CENTER");
}
if ((channelConfig & 0x00000800) != 0) {
Log.v(TAG, " CHANNEL_OUT_SIDE_LEFT");
}
if ((channelConfig & 0x00001000) != 0) {
Log.v(TAG, " CHANNEL_OUT_SIDE_RIGHT");
}
*/
}
frameSize = (sampleSize * desiredChannels);
// Let the user pick a larger buffer if they really want -- but ye
// gods they probably shouldn't, the minimums are horrifyingly high
// latency already
int minBufferSize;
if (isCapture) {
minBufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat);
} else {
minBufferSize = AudioTrack.getMinBufferSize(sampleRate, channelConfig, audioFormat);
}
desiredFrames = Math.max(desiredFrames, (minBufferSize + frameSize - 1) / frameSize);
int[] results = new int[4];
if (isCapture) {
if (mAudioRecord == null) {
mAudioRecord = new AudioRecord(MediaRecorder.AudioSource.DEFAULT, sampleRate,
channelConfig, audioFormat, desiredFrames * frameSize);
// see notes about AudioTrack state in audioOpen(), above. Probably also applies here.
if (mAudioRecord.getState() != AudioRecord.STATE_INITIALIZED) {
Log.e(TAG, "Failed during initialization of AudioRecord");
mAudioRecord.release();
mAudioRecord = null;
return null;
}
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */ && deviceId != 0) {
mAudioRecord.setPreferredDevice(getOutputAudioDeviceInfo(deviceId));
}
mAudioRecord.startRecording();
}
results[0] = mAudioRecord.getSampleRate();
results[1] = mAudioRecord.getAudioFormat();
results[2] = mAudioRecord.getChannelCount();
} else {
if (mAudioTrack == null) {
mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, sampleRate, channelConfig, audioFormat, desiredFrames * frameSize, AudioTrack.MODE_STREAM);
// Instantiating AudioTrack can "succeed" without an exception and the track may still be invalid
// Ref: https://android.googlesource.com/platform/frameworks/base/+/refs/heads/master/media/java/android/media/AudioTrack.java
// Ref: http://developer.android.com/reference/android/media/AudioTrack.html#getState()
if (mAudioTrack.getState() != AudioTrack.STATE_INITIALIZED) {
/* Try again, with safer values */
Log.e(TAG, "Failed during initialization of Audio Track");
mAudioTrack.release();
mAudioTrack = null;
return null;
}
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */ && deviceId != 0) {
mAudioTrack.setPreferredDevice(getInputAudioDeviceInfo(deviceId));
}
mAudioTrack.play();
}
results[0] = mAudioTrack.getSampleRate();
results[1] = mAudioTrack.getAudioFormat();
results[2] = mAudioTrack.getChannelCount();
}
results[3] = desiredFrames;
Log.v(TAG, "Opening " + (isCapture ? "capture" : "playback") + ", got " + results[3] + " frames of " + results[2] + " channel " + getAudioFormatString(results[1]) + " audio at " + results[0] + " Hz");
return results;
}
private static AudioDeviceInfo getInputAudioDeviceInfo(int deviceId) {
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) {
AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
return Arrays.stream(audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS))
.filter(deviceInfo -> deviceInfo.getId() == deviceId)
.findFirst()
.orElse(null);
} else {
return null;
}
}
private static AudioDeviceInfo getOutputAudioDeviceInfo(int deviceId) {
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) {
AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
return Arrays.stream(audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS))
.filter(deviceInfo -> deviceInfo.getId() == deviceId)
.findFirst()
.orElse(null);
} else {
return null;
}
}
private static void registerAudioDeviceCallback() {
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) {
AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
audioManager.registerAudioDeviceCallback(mAudioDeviceCallback, null);
}
}
private static void unregisterAudioDeviceCallback(Context context) {
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) {
AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
audioManager.unregisterAudioDeviceCallback(mAudioDeviceCallback);
}
}
/**
* This method is called by SDL using JNI.
*/
public static int[] getAudioOutputDevices() {
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) {
AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
return Arrays.stream(audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)).mapToInt(AudioDeviceInfo::getId).toArray();
} else {
return NO_DEVICES;
}
}
/**
* This method is called by SDL using JNI.
*/
public static int[] getAudioInputDevices() {
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) {
AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
return Arrays.stream(audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)).mapToInt(AudioDeviceInfo::getId).toArray();
} else {
return NO_DEVICES;
}
}
/**
* This method is called by SDL using JNI.
*/
public static int[] audioOpen(int sampleRate, int audioFormat, int desiredChannels, int desiredFrames, int deviceId) {
return open(false, sampleRate, audioFormat, desiredChannels, desiredFrames, deviceId);
}
/**
* This method is called by SDL using JNI.
*/
public static void audioWriteFloatBuffer(float[] buffer) {
if (mAudioTrack == null) {
Log.e(TAG, "Attempted to make audio call with uninitialized audio!");
return;
}
if (android.os.Build.VERSION.SDK_INT < 21 /* Android 5.0 (LOLLIPOP) */) {
Log.e(TAG, "Attempted to make an incompatible audio call with uninitialized audio! (floating-point output is supported since Android 5.0 Lollipop)");
return;
}
for (int i = 0; i < buffer.length;) {
int result = mAudioTrack.write(buffer, i, buffer.length - i, AudioTrack.WRITE_BLOCKING);
if (result > 0) {
i += result;
} else if (result == 0) {
try {
Thread.sleep(1);
} catch(InterruptedException e) {
// Nom nom
}
} else {
Log.w(TAG, "SDL audio: error return from write(float)");
return;
}
}
}
/**
* This method is called by SDL using JNI.
*/
public static void audioWriteShortBuffer(short[] buffer) {
if (mAudioTrack == null) {
Log.e(TAG, "Attempted to make audio call with uninitialized audio!");
return;
}
for (int i = 0; i < buffer.length;) {
int result = mAudioTrack.write(buffer, i, buffer.length - i);
if (result > 0) {
i += result;
} else if (result == 0) {
try {
Thread.sleep(1);
} catch(InterruptedException e) {
// Nom nom
}
} else {
Log.w(TAG, "SDL audio: error return from write(short)");
return;
}
}
}
/**
* This method is called by SDL using JNI.
*/
public static void audioWriteByteBuffer(byte[] buffer) {
if (mAudioTrack == null) {
Log.e(TAG, "Attempted to make audio call with uninitialized audio!");
return;
}
for (int i = 0; i < buffer.length; ) {
int result = mAudioTrack.write(buffer, i, buffer.length - i);
if (result > 0) {
i += result;
} else if (result == 0) {
try {
Thread.sleep(1);
} catch(InterruptedException e) {
// Nom nom
}
} else {
Log.w(TAG, "SDL audio: error return from write(byte)");
return;
}
}
}
/**
* This method is called by SDL using JNI.
*/
public static int[] captureOpen(int sampleRate, int audioFormat, int desiredChannels, int desiredFrames, int deviceId) {
return open(true, sampleRate, audioFormat, desiredChannels, desiredFrames, deviceId);
}
/** This method is called by SDL using JNI. */
public static int captureReadFloatBuffer(float[] buffer, boolean blocking) {
if (Build.VERSION.SDK_INT < 23 /* Android 6.0 (M) */) {
return 0;
} else {
return mAudioRecord.read(buffer, 0, buffer.length, blocking ? AudioRecord.READ_BLOCKING : AudioRecord.READ_NON_BLOCKING);
}
}
/** This method is called by SDL using JNI. */
public static int captureReadShortBuffer(short[] buffer, boolean blocking) {
if (Build.VERSION.SDK_INT < 23 /* Android 6.0 (M) */) {
return mAudioRecord.read(buffer, 0, buffer.length);
} else {
return mAudioRecord.read(buffer, 0, buffer.length, blocking ? AudioRecord.READ_BLOCKING : AudioRecord.READ_NON_BLOCKING);
}
}
/** This method is called by SDL using JNI. */
public static int captureReadByteBuffer(byte[] buffer, boolean blocking) {
if (Build.VERSION.SDK_INT < 23 /* Android 6.0 (M) */) {
return mAudioRecord.read(buffer, 0, buffer.length);
} else {
return mAudioRecord.read(buffer, 0, buffer.length, blocking ? AudioRecord.READ_BLOCKING : AudioRecord.READ_NON_BLOCKING);
}
}
/** This method is called by SDL using JNI. */
public static void audioClose() {
if (mAudioTrack != null) {
mAudioTrack.stop();
mAudioTrack.release();
mAudioTrack = null;
}
}
/** This method is called by SDL using JNI. */
public static void captureClose() {
if (mAudioRecord != null) {
mAudioRecord.stop();
mAudioRecord.release();
mAudioRecord = null;
}
}
/** This method is called by SDL using JNI. */
public static void audioSetThreadPriority(boolean iscapture, int device_id) {
try {
/* Set thread name */
if (iscapture) {
Thread.currentThread().setName("SDLAudioC" + device_id);
} else {
Thread.currentThread().setName("SDLAudioP" + device_id);
}
/* Set thread priority */
android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_AUDIO);
} catch (Exception e) {
Log.v(TAG, "modify thread properties failed " + e.toString());
}
}
public static native int nativeSetupJNI();
public static native void removeAudioDevice(boolean isCapture, int deviceId);
public static native void addAudioDevice(boolean isCapture, int deviceId);
}
@@ -1,856 +0,0 @@
package org.libsdl.app;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import android.content.Context;
import android.os.Build;
import android.os.VibrationEffect;
import android.os.Vibrator;
import android.util.Log;
import android.view.InputDevice;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
public class SDLControllerManager
{
public static native int nativeSetupJNI();
public static native int nativeAddJoystick(int device_id, String name, String desc,
int vendor_id, int product_id,
boolean is_accelerometer, int button_mask,
int naxes, int axis_mask, int nhats, int nballs);
public static native int nativeRemoveJoystick(int device_id);
public static native int nativeAddHaptic(int device_id, String name);
public static native int nativeRemoveHaptic(int device_id);
public static native int onNativePadDown(int device_id, int keycode);
public static native int onNativePadUp(int device_id, int keycode);
public static native void onNativeJoy(int device_id, int axis,
float value);
public static native void onNativeHat(int device_id, int hat_id,
int x, int y);
protected static SDLJoystickHandler mJoystickHandler;
protected static SDLHapticHandler mHapticHandler;
private static final String TAG = "SDLControllerManager";
public static void initialize() {
if (mJoystickHandler == null) {
if (Build.VERSION.SDK_INT >= 19 /* Android 4.4 (KITKAT) */) {
mJoystickHandler = new SDLJoystickHandler_API19();
} else {
mJoystickHandler = new SDLJoystickHandler_API16();
}
}
if (mHapticHandler == null) {
if (Build.VERSION.SDK_INT >= 26 /* Android 8.0 (O) */) {
mHapticHandler = new SDLHapticHandler_API26();
} else {
mHapticHandler = new SDLHapticHandler();
}
}
}
// Joystick glue code, just a series of stubs that redirect to the SDLJoystickHandler instance
public static boolean handleJoystickMotionEvent(MotionEvent event) {
return mJoystickHandler.handleMotionEvent(event);
}
/**
* This method is called by SDL using JNI.
*/
public static void pollInputDevices() {
mJoystickHandler.pollInputDevices();
}
/**
* This method is called by SDL using JNI.
*/
public static void pollHapticDevices() {
mHapticHandler.pollHapticDevices();
}
/**
* This method is called by SDL using JNI.
*/
public static void hapticRun(int device_id, float intensity, int length) {
mHapticHandler.run(device_id, intensity, length);
}
/**
* This method is called by SDL using JNI.
*/
public static void hapticStop(int device_id)
{
mHapticHandler.stop(device_id);
}
// Check if a given device is considered a possible SDL joystick
public static boolean isDeviceSDLJoystick(int deviceId) {
InputDevice device = InputDevice.getDevice(deviceId);
// We cannot use InputDevice.isVirtual before API 16, so let's accept
// only nonnegative device ids (VIRTUAL_KEYBOARD equals -1)
if ((device == null) || (deviceId < 0)) {
return false;
}
int sources = device.getSources();
/* This is called for every button press, so let's not spam the logs */
/*
if ((sources & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) {
Log.v(TAG, "Input device " + device.getName() + " has class joystick.");
}
if ((sources & InputDevice.SOURCE_DPAD) == InputDevice.SOURCE_DPAD) {
Log.v(TAG, "Input device " + device.getName() + " is a dpad.");
}
if ((sources & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) {
Log.v(TAG, "Input device " + device.getName() + " is a gamepad.");
}
*/
return ((sources & InputDevice.SOURCE_CLASS_JOYSTICK) != 0 ||
((sources & InputDevice.SOURCE_DPAD) == InputDevice.SOURCE_DPAD) ||
((sources & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD)
);
}
}
class SDLJoystickHandler {
/**
* Handles given MotionEvent.
* @param event the event to be handled.
* @return if given event was processed.
*/
public boolean handleMotionEvent(MotionEvent event) {
return false;
}
/**
* Handles adding and removing of input devices.
*/
public void pollInputDevices() {
}
}
/* Actual joystick functionality available for API >= 12 devices */
class SDLJoystickHandler_API16 extends SDLJoystickHandler {
static class SDLJoystick {
public int device_id;
public String name;
public String desc;
public ArrayList<InputDevice.MotionRange> axes;
public ArrayList<InputDevice.MotionRange> hats;
}
static class RangeComparator implements Comparator<InputDevice.MotionRange> {
@Override
public int compare(InputDevice.MotionRange arg0, InputDevice.MotionRange arg1) {
// Some controllers, like the Moga Pro 2, return AXIS_GAS (22) for right trigger and AXIS_BRAKE (23) for left trigger - swap them so they're sorted in the right order for SDL
int arg0Axis = arg0.getAxis();
int arg1Axis = arg1.getAxis();
if (arg0Axis == MotionEvent.AXIS_GAS) {
arg0Axis = MotionEvent.AXIS_BRAKE;
} else if (arg0Axis == MotionEvent.AXIS_BRAKE) {
arg0Axis = MotionEvent.AXIS_GAS;
}
if (arg1Axis == MotionEvent.AXIS_GAS) {
arg1Axis = MotionEvent.AXIS_BRAKE;
} else if (arg1Axis == MotionEvent.AXIS_BRAKE) {
arg1Axis = MotionEvent.AXIS_GAS;
}
// Make sure the AXIS_Z is sorted between AXIS_RY and AXIS_RZ.
// This is because the usual pairing are:
// - AXIS_X + AXIS_Y (left stick).
// - AXIS_RX, AXIS_RY (sometimes the right stick, sometimes triggers).
// - AXIS_Z, AXIS_RZ (sometimes the right stick, sometimes triggers).
// This sorts the axes in the above order, which tends to be correct
// for Xbox-ish game pads that have the right stick on RX/RY and the
// triggers on Z/RZ.
//
// Gamepads that don't have AXIS_Z/AXIS_RZ but use
// AXIS_LTRIGGER/AXIS_RTRIGGER are unaffected by this.
//
// References:
// - https://developer.android.com/develop/ui/views/touch-and-input/game-controllers/controller-input
// - https://www.kernel.org/doc/html/latest/input/gamepad.html
if (arg0Axis == MotionEvent.AXIS_Z) {
arg0Axis = MotionEvent.AXIS_RZ - 1;
} else if (arg0Axis > MotionEvent.AXIS_Z && arg0Axis < MotionEvent.AXIS_RZ) {
--arg0Axis;
}
if (arg1Axis == MotionEvent.AXIS_Z) {
arg1Axis = MotionEvent.AXIS_RZ - 1;
} else if (arg1Axis > MotionEvent.AXIS_Z && arg1Axis < MotionEvent.AXIS_RZ) {
--arg1Axis;
}
return arg0Axis - arg1Axis;
}
}
private final ArrayList<SDLJoystick> mJoysticks;
public SDLJoystickHandler_API16() {
mJoysticks = new ArrayList<SDLJoystick>();
}
@Override
public void pollInputDevices() {
int[] deviceIds = InputDevice.getDeviceIds();
for (int device_id : deviceIds) {
if (SDLControllerManager.isDeviceSDLJoystick(device_id)) {
SDLJoystick joystick = getJoystick(device_id);
if (joystick == null) {
InputDevice joystickDevice = InputDevice.getDevice(device_id);
joystick = new SDLJoystick();
joystick.device_id = device_id;
joystick.name = joystickDevice.getName();
joystick.desc = getJoystickDescriptor(joystickDevice);
joystick.axes = new ArrayList<InputDevice.MotionRange>();
joystick.hats = new ArrayList<InputDevice.MotionRange>();
List<InputDevice.MotionRange> ranges = joystickDevice.getMotionRanges();
Collections.sort(ranges, new RangeComparator());
for (InputDevice.MotionRange range : ranges) {
if ((range.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) {
if (range.getAxis() == MotionEvent.AXIS_HAT_X || range.getAxis() == MotionEvent.AXIS_HAT_Y) {
joystick.hats.add(range);
} else {
joystick.axes.add(range);
}
}
}
mJoysticks.add(joystick);
SDLControllerManager.nativeAddJoystick(joystick.device_id, joystick.name, joystick.desc,
getVendorId(joystickDevice), getProductId(joystickDevice), false,
getButtonMask(joystickDevice), joystick.axes.size(), getAxisMask(joystick.axes), joystick.hats.size()/2, 0);
}
}
}
/* Check removed devices */
ArrayList<Integer> removedDevices = null;
for (SDLJoystick joystick : mJoysticks) {
int device_id = joystick.device_id;
int i;
for (i = 0; i < deviceIds.length; i++) {
if (device_id == deviceIds[i]) break;
}
if (i == deviceIds.length) {
if (removedDevices == null) {
removedDevices = new ArrayList<Integer>();
}
removedDevices.add(device_id);
}
}
if (removedDevices != null) {
for (int device_id : removedDevices) {
SDLControllerManager.nativeRemoveJoystick(device_id);
for (int i = 0; i < mJoysticks.size(); i++) {
if (mJoysticks.get(i).device_id == device_id) {
mJoysticks.remove(i);
break;
}
}
}
}
}
protected SDLJoystick getJoystick(int device_id) {
for (SDLJoystick joystick : mJoysticks) {
if (joystick.device_id == device_id) {
return joystick;
}
}
return null;
}
@Override
public boolean handleMotionEvent(MotionEvent event) {
int actionPointerIndex = event.getActionIndex();
int action = event.getActionMasked();
if (action == MotionEvent.ACTION_MOVE) {
SDLJoystick joystick = getJoystick(event.getDeviceId());
if (joystick != null) {
for (int i = 0; i < joystick.axes.size(); i++) {
InputDevice.MotionRange range = joystick.axes.get(i);
/* Normalize the value to -1...1 */
float value = (event.getAxisValue(range.getAxis(), actionPointerIndex) - range.getMin()) / range.getRange() * 2.0f - 1.0f;
SDLControllerManager.onNativeJoy(joystick.device_id, i, value);
}
for (int i = 0; i < joystick.hats.size() / 2; i++) {
int hatX = Math.round(event.getAxisValue(joystick.hats.get(2 * i).getAxis(), actionPointerIndex));
int hatY = Math.round(event.getAxisValue(joystick.hats.get(2 * i + 1).getAxis(), actionPointerIndex));
SDLControllerManager.onNativeHat(joystick.device_id, i, hatX, hatY);
}
}
}
return true;
}
public String getJoystickDescriptor(InputDevice joystickDevice) {
String desc = joystickDevice.getDescriptor();
if (desc != null && !desc.isEmpty()) {
return desc;
}
return joystickDevice.getName();
}
public int getProductId(InputDevice joystickDevice) {
return 0;
}
public int getVendorId(InputDevice joystickDevice) {
return 0;
}
public int getAxisMask(List<InputDevice.MotionRange> ranges) {
return -1;
}
public int getButtonMask(InputDevice joystickDevice) {
return -1;
}
}
class SDLJoystickHandler_API19 extends SDLJoystickHandler_API16 {
@Override
public int getProductId(InputDevice joystickDevice) {
return joystickDevice.getProductId();
}
@Override
public int getVendorId(InputDevice joystickDevice) {
return joystickDevice.getVendorId();
}
@Override
public int getAxisMask(List<InputDevice.MotionRange> ranges) {
// For compatibility, keep computing the axis mask like before,
// only really distinguishing 2, 4 and 6 axes.
int axis_mask = 0;
if (ranges.size() >= 2) {
// ((1 << SDL_GAMEPAD_AXIS_LEFTX) | (1 << SDL_GAMEPAD_AXIS_LEFTY))
axis_mask |= 0x0003;
}
if (ranges.size() >= 4) {
// ((1 << SDL_GAMEPAD_AXIS_RIGHTX) | (1 << SDL_GAMEPAD_AXIS_RIGHTY))
axis_mask |= 0x000c;
}
if (ranges.size() >= 6) {
// ((1 << SDL_GAMEPAD_AXIS_LEFT_TRIGGER) | (1 << SDL_GAMEPAD_AXIS_RIGHT_TRIGGER))
axis_mask |= 0x0030;
}
// Also add an indicator bit for whether the sorting order has changed.
// This serves to disable outdated gamecontrollerdb.txt mappings.
boolean have_z = false;
boolean have_past_z_before_rz = false;
for (InputDevice.MotionRange range : ranges) {
int axis = range.getAxis();
if (axis == MotionEvent.AXIS_Z) {
have_z = true;
} else if (axis > MotionEvent.AXIS_Z && axis < MotionEvent.AXIS_RZ) {
have_past_z_before_rz = true;
}
}
if (have_z && have_past_z_before_rz) {
// If both these exist, the compare() function changed sorting order.
// Set a bit to indicate this fact.
axis_mask |= 0x8000;
}
return axis_mask;
}
@Override
public int getButtonMask(InputDevice joystickDevice) {
int button_mask = 0;
int[] keys = new int[] {
KeyEvent.KEYCODE_BUTTON_A,
KeyEvent.KEYCODE_BUTTON_B,
KeyEvent.KEYCODE_BUTTON_X,
KeyEvent.KEYCODE_BUTTON_Y,
KeyEvent.KEYCODE_BACK,
KeyEvent.KEYCODE_MENU,
KeyEvent.KEYCODE_BUTTON_MODE,
KeyEvent.KEYCODE_BUTTON_START,
KeyEvent.KEYCODE_BUTTON_THUMBL,
KeyEvent.KEYCODE_BUTTON_THUMBR,
KeyEvent.KEYCODE_BUTTON_L1,
KeyEvent.KEYCODE_BUTTON_R1,
KeyEvent.KEYCODE_DPAD_UP,
KeyEvent.KEYCODE_DPAD_DOWN,
KeyEvent.KEYCODE_DPAD_LEFT,
KeyEvent.KEYCODE_DPAD_RIGHT,
KeyEvent.KEYCODE_BUTTON_SELECT,
KeyEvent.KEYCODE_DPAD_CENTER,
// These don't map into any SDL controller buttons directly
KeyEvent.KEYCODE_BUTTON_L2,
KeyEvent.KEYCODE_BUTTON_R2,
KeyEvent.KEYCODE_BUTTON_C,
KeyEvent.KEYCODE_BUTTON_Z,
KeyEvent.KEYCODE_BUTTON_1,
KeyEvent.KEYCODE_BUTTON_2,
KeyEvent.KEYCODE_BUTTON_3,
KeyEvent.KEYCODE_BUTTON_4,
KeyEvent.KEYCODE_BUTTON_5,
KeyEvent.KEYCODE_BUTTON_6,
KeyEvent.KEYCODE_BUTTON_7,
KeyEvent.KEYCODE_BUTTON_8,
KeyEvent.KEYCODE_BUTTON_9,
KeyEvent.KEYCODE_BUTTON_10,
KeyEvent.KEYCODE_BUTTON_11,
KeyEvent.KEYCODE_BUTTON_12,
KeyEvent.KEYCODE_BUTTON_13,
KeyEvent.KEYCODE_BUTTON_14,
KeyEvent.KEYCODE_BUTTON_15,
KeyEvent.KEYCODE_BUTTON_16,
};
int[] masks = new int[] {
(1 << 0), // A -> A
(1 << 1), // B -> B
(1 << 2), // X -> X
(1 << 3), // Y -> Y
(1 << 4), // BACK -> BACK
(1 << 6), // MENU -> START
(1 << 5), // MODE -> GUIDE
(1 << 6), // START -> START
(1 << 7), // THUMBL -> LEFTSTICK
(1 << 8), // THUMBR -> RIGHTSTICK
(1 << 9), // L1 -> LEFTSHOULDER
(1 << 10), // R1 -> RIGHTSHOULDER
(1 << 11), // DPAD_UP -> DPAD_UP
(1 << 12), // DPAD_DOWN -> DPAD_DOWN
(1 << 13), // DPAD_LEFT -> DPAD_LEFT
(1 << 14), // DPAD_RIGHT -> DPAD_RIGHT
(1 << 4), // SELECT -> BACK
(1 << 0), // DPAD_CENTER -> A
(1 << 15), // L2 -> ??
(1 << 16), // R2 -> ??
(1 << 17), // C -> ??
(1 << 18), // Z -> ??
(1 << 20), // 1 -> ??
(1 << 21), // 2 -> ??
(1 << 22), // 3 -> ??
(1 << 23), // 4 -> ??
(1 << 24), // 5 -> ??
(1 << 25), // 6 -> ??
(1 << 26), // 7 -> ??
(1 << 27), // 8 -> ??
(1 << 28), // 9 -> ??
(1 << 29), // 10 -> ??
(1 << 30), // 11 -> ??
(1 << 31), // 12 -> ??
// We're out of room...
0xFFFFFFFF, // 13 -> ??
0xFFFFFFFF, // 14 -> ??
0xFFFFFFFF, // 15 -> ??
0xFFFFFFFF, // 16 -> ??
};
boolean[] has_keys = joystickDevice.hasKeys(keys);
for (int i = 0; i < keys.length; ++i) {
if (has_keys[i]) {
button_mask |= masks[i];
}
}
return button_mask;
}
}
class SDLHapticHandler_API26 extends SDLHapticHandler {
@Override
public void run(int device_id, float intensity, int length) {
SDLHaptic haptic = getHaptic(device_id);
if (haptic != null) {
Log.d("SDL", "Rtest: Vibe with intensity " + intensity + " for " + length);
if (intensity == 0.0f) {
stop(device_id);
return;
}
int vibeValue = Math.round(intensity * 255);
if (vibeValue > 255) {
vibeValue = 255;
}
if (vibeValue < 1) {
stop(device_id);
return;
}
try {
haptic.vib.vibrate(VibrationEffect.createOneShot(length, vibeValue));
}
catch (Exception e) {
// Fall back to the generic method, which uses DEFAULT_AMPLITUDE, but works even if
// something went horribly wrong with the Android 8.0 APIs.
haptic.vib.vibrate(length);
}
}
}
}
class SDLHapticHandler {
static class SDLHaptic {
public int device_id;
public String name;
public Vibrator vib;
}
private final ArrayList<SDLHaptic> mHaptics;
public SDLHapticHandler() {
mHaptics = new ArrayList<SDLHaptic>();
}
public void run(int device_id, float intensity, int length) {
SDLHaptic haptic = getHaptic(device_id);
if (haptic != null) {
haptic.vib.vibrate(length);
}
}
public void stop(int device_id) {
SDLHaptic haptic = getHaptic(device_id);
if (haptic != null) {
haptic.vib.cancel();
}
}
public void pollHapticDevices() {
final int deviceId_VIBRATOR_SERVICE = 999999;
boolean hasVibratorService = false;
int[] deviceIds = InputDevice.getDeviceIds();
// It helps processing the device ids in reverse order
// For example, in the case of the XBox 360 wireless dongle,
// so the first controller seen by SDL matches what the receiver
// considers to be the first controller
for (int i = deviceIds.length - 1; i > -1; i--) {
SDLHaptic haptic = getHaptic(deviceIds[i]);
if (haptic == null) {
InputDevice device = InputDevice.getDevice(deviceIds[i]);
Vibrator vib = device.getVibrator();
if (vib != null) {
if (vib.hasVibrator()) {
haptic = new SDLHaptic();
haptic.device_id = deviceIds[i];
haptic.name = device.getName();
haptic.vib = vib;
mHaptics.add(haptic);
SDLControllerManager.nativeAddHaptic(haptic.device_id, haptic.name);
}
}
}
}
/* Check VIBRATOR_SERVICE */
Vibrator vib = (Vibrator) SDL.getContext().getSystemService(Context.VIBRATOR_SERVICE);
if (vib != null) {
hasVibratorService = vib.hasVibrator();
if (hasVibratorService) {
SDLHaptic haptic = getHaptic(deviceId_VIBRATOR_SERVICE);
if (haptic == null) {
haptic = new SDLHaptic();
haptic.device_id = deviceId_VIBRATOR_SERVICE;
haptic.name = "VIBRATOR_SERVICE";
haptic.vib = vib;
mHaptics.add(haptic);
SDLControllerManager.nativeAddHaptic(haptic.device_id, haptic.name);
}
}
}
/* Check removed devices */
ArrayList<Integer> removedDevices = null;
for (SDLHaptic haptic : mHaptics) {
int device_id = haptic.device_id;
int i;
for (i = 0; i < deviceIds.length; i++) {
if (device_id == deviceIds[i]) break;
}
if (device_id != deviceId_VIBRATOR_SERVICE || !hasVibratorService) {
if (i == deviceIds.length) {
if (removedDevices == null) {
removedDevices = new ArrayList<Integer>();
}
removedDevices.add(device_id);
}
} // else: don't remove the vibrator if it is still present
}
if (removedDevices != null) {
for (int device_id : removedDevices) {
SDLControllerManager.nativeRemoveHaptic(device_id);
for (int i = 0; i < mHaptics.size(); i++) {
if (mHaptics.get(i).device_id == device_id) {
mHaptics.remove(i);
break;
}
}
}
}
}
protected SDLHaptic getHaptic(int device_id) {
for (SDLHaptic haptic : mHaptics) {
if (haptic.device_id == device_id) {
return haptic;
}
}
return null;
}
}
class SDLGenericMotionListener_API12 implements View.OnGenericMotionListener {
// Generic Motion (mouse hover, joystick...) events go here
@Override
public boolean onGenericMotion(View v, MotionEvent event) {
float x, y;
int action;
switch ( event.getSource() ) {
case InputDevice.SOURCE_JOYSTICK:
return SDLControllerManager.handleJoystickMotionEvent(event);
case InputDevice.SOURCE_MOUSE:
action = event.getActionMasked();
switch (action) {
case MotionEvent.ACTION_SCROLL:
x = event.getAxisValue(MotionEvent.AXIS_HSCROLL, 0);
y = event.getAxisValue(MotionEvent.AXIS_VSCROLL, 0);
SDLActivity.onNativeMouse(0, action, x, y, false);
return true;
case MotionEvent.ACTION_HOVER_MOVE:
x = event.getX(0);
y = event.getY(0);
SDLActivity.onNativeMouse(0, action, x, y, false);
return true;
default:
break;
}
break;
default:
break;
}
// Event was not managed
return false;
}
public boolean supportsRelativeMouse() {
return false;
}
public boolean inRelativeMode() {
return false;
}
public boolean setRelativeMouseEnabled(boolean enabled) {
return false;
}
public void reclaimRelativeMouseModeIfNeeded()
{
}
public float getEventX(MotionEvent event) {
return event.getX(0);
}
public float getEventY(MotionEvent event) {
return event.getY(0);
}
}
class SDLGenericMotionListener_API24 extends SDLGenericMotionListener_API12 {
// Generic Motion (mouse hover, joystick...) events go here
private boolean mRelativeModeEnabled;
@Override
public boolean onGenericMotion(View v, MotionEvent event) {
// Handle relative mouse mode
if (mRelativeModeEnabled) {
if (event.getSource() == InputDevice.SOURCE_MOUSE) {
int action = event.getActionMasked();
if (action == MotionEvent.ACTION_HOVER_MOVE) {
float x = event.getAxisValue(MotionEvent.AXIS_RELATIVE_X);
float y = event.getAxisValue(MotionEvent.AXIS_RELATIVE_Y);
SDLActivity.onNativeMouse(0, action, x, y, true);
return true;
}
}
}
// Event was not managed, call SDLGenericMotionListener_API12 method
return super.onGenericMotion(v, event);
}
@Override
public boolean supportsRelativeMouse() {
return true;
}
@Override
public boolean inRelativeMode() {
return mRelativeModeEnabled;
}
@Override
public boolean setRelativeMouseEnabled(boolean enabled) {
mRelativeModeEnabled = enabled;
return true;
}
@Override
public float getEventX(MotionEvent event) {
if (mRelativeModeEnabled) {
return event.getAxisValue(MotionEvent.AXIS_RELATIVE_X);
} else {
return event.getX(0);
}
}
@Override
public float getEventY(MotionEvent event) {
if (mRelativeModeEnabled) {
return event.getAxisValue(MotionEvent.AXIS_RELATIVE_Y);
} else {
return event.getY(0);
}
}
}
class SDLGenericMotionListener_API26 extends SDLGenericMotionListener_API24 {
// Generic Motion (mouse hover, joystick...) events go here
private boolean mRelativeModeEnabled;
@Override
public boolean onGenericMotion(View v, MotionEvent event) {
float x, y;
int action;
switch ( event.getSource() ) {
case InputDevice.SOURCE_JOYSTICK:
return SDLControllerManager.handleJoystickMotionEvent(event);
case InputDevice.SOURCE_MOUSE:
// DeX desktop mouse cursor is a separate non-standard input type.
case InputDevice.SOURCE_MOUSE | InputDevice.SOURCE_TOUCHSCREEN:
action = event.getActionMasked();
switch (action) {
case MotionEvent.ACTION_SCROLL:
x = event.getAxisValue(MotionEvent.AXIS_HSCROLL, 0);
y = event.getAxisValue(MotionEvent.AXIS_VSCROLL, 0);
SDLActivity.onNativeMouse(0, action, x, y, false);
return true;
case MotionEvent.ACTION_HOVER_MOVE:
x = event.getX(0);
y = event.getY(0);
SDLActivity.onNativeMouse(0, action, x, y, false);
return true;
default:
break;
}
break;
case InputDevice.SOURCE_MOUSE_RELATIVE:
action = event.getActionMasked();
switch (action) {
case MotionEvent.ACTION_SCROLL:
x = event.getAxisValue(MotionEvent.AXIS_HSCROLL, 0);
y = event.getAxisValue(MotionEvent.AXIS_VSCROLL, 0);
SDLActivity.onNativeMouse(0, action, x, y, false);
return true;
case MotionEvent.ACTION_HOVER_MOVE:
x = event.getX(0);
y = event.getY(0);
SDLActivity.onNativeMouse(0, action, x, y, true);
return true;
default:
break;
}
break;
default:
break;
}
// Event was not managed
return false;
}
@Override
public boolean supportsRelativeMouse() {
return (!SDLActivity.isDeXMode() || Build.VERSION.SDK_INT >= 27 /* Android 8.1 (O_MR1) */);
}
@Override
public boolean inRelativeMode() {
return mRelativeModeEnabled;
}
@Override
public boolean setRelativeMouseEnabled(boolean enabled) {
if (!SDLActivity.isDeXMode() || Build.VERSION.SDK_INT >= 27 /* Android 8.1 (O_MR1) */) {
if (enabled) {
SDLActivity.getContentView().requestPointerCapture();
} else {
SDLActivity.getContentView().releasePointerCapture();
}
mRelativeModeEnabled = enabled;
return true;
} else {
return false;
}
}
@Override
public void reclaimRelativeMouseModeIfNeeded()
{
if (mRelativeModeEnabled && !SDLActivity.isDeXMode()) {
SDLActivity.getContentView().requestPointerCapture();
}
}
@Override
public float getEventX(MotionEvent event) {
// Relative mouse in capture mode will only have relative for X/Y
return event.getX(0);
}
@Override
public float getEventY(MotionEvent event) {
// Relative mouse in capture mode will only have relative for X/Y
return event.getY(0);
}
}
@@ -1,405 +0,0 @@
package org.libsdl.app;
import android.content.Context;
import android.content.pm.ActivityInfo;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Build;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.Display;
import android.view.InputDevice;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.view.WindowManager;
/**
SDLSurface. This is what we draw on, so we need to know when it's created
in order to do anything useful.
Because of this, that's where we set up the SDL thread
*/
public class SDLSurface extends SurfaceView implements SurfaceHolder.Callback,
View.OnKeyListener, View.OnTouchListener, SensorEventListener {
// Sensors
protected SensorManager mSensorManager;
protected Display mDisplay;
// Keep track of the surface size to normalize touch events
protected float mWidth, mHeight;
// Is SurfaceView ready for rendering
public boolean mIsSurfaceReady;
// Startup
public SDLSurface(Context context) {
super(context);
getHolder().addCallback(this);
setFocusable(true);
setFocusableInTouchMode(true);
requestFocus();
setOnKeyListener(this);
setOnTouchListener(this);
mDisplay = ((WindowManager)context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
mSensorManager = (SensorManager)context.getSystemService(Context.SENSOR_SERVICE);
setOnGenericMotionListener(SDLActivity.getMotionListener());
// Some arbitrary defaults to avoid a potential division by zero
mWidth = 1.0f;
mHeight = 1.0f;
mIsSurfaceReady = false;
}
public void handlePause() {
enableSensor(Sensor.TYPE_ACCELEROMETER, false);
}
public void handleResume() {
setFocusable(true);
setFocusableInTouchMode(true);
requestFocus();
setOnKeyListener(this);
setOnTouchListener(this);
enableSensor(Sensor.TYPE_ACCELEROMETER, true);
}
public Surface getNativeSurface() {
return getHolder().getSurface();
}
// Called when we have a valid drawing surface
@Override
public void surfaceCreated(SurfaceHolder holder) {
Log.v("SDL", "surfaceCreated()");
SDLActivity.onNativeSurfaceCreated();
}
// Called when we lose the surface
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
Log.v("SDL", "surfaceDestroyed()");
// Transition to pause, if needed
SDLActivity.mNextNativeState = SDLActivity.NativeState.PAUSED;
SDLActivity.handleNativeState();
mIsSurfaceReady = false;
SDLActivity.onNativeSurfaceDestroyed();
}
// Called when the surface is resized
@Override
public void surfaceChanged(SurfaceHolder holder,
int format, int width, int height) {
Log.v("SDL", "surfaceChanged()");
if (SDLActivity.mSingleton == null) {
return;
}
mWidth = width;
mHeight = height;
int nDeviceWidth = width;
int nDeviceHeight = height;
try
{
if (Build.VERSION.SDK_INT >= 17 /* Android 4.2 (JELLY_BEAN_MR1) */) {
DisplayMetrics realMetrics = new DisplayMetrics();
mDisplay.getRealMetrics( realMetrics );
nDeviceWidth = realMetrics.widthPixels;
nDeviceHeight = realMetrics.heightPixels;
}
} catch(Exception ignored) {
}
synchronized(SDLActivity.getContext()) {
// In case we're waiting on a size change after going fullscreen, send a notification.
SDLActivity.getContext().notifyAll();
}
Log.v("SDL", "Window size: " + width + "x" + height);
Log.v("SDL", "Device size: " + nDeviceWidth + "x" + nDeviceHeight);
SDLActivity.nativeSetScreenResolution(width, height, nDeviceWidth, nDeviceHeight, mDisplay.getRefreshRate());
SDLActivity.onNativeResize();
// Prevent a screen distortion glitch,
// for instance when the device is in Landscape and a Portrait App is resumed.
boolean skip = false;
int requestedOrientation = SDLActivity.mSingleton.getRequestedOrientation();
if (requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT || requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT) {
if (mWidth > mHeight) {
skip = true;
}
} else if (requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE || requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE) {
if (mWidth < mHeight) {
skip = true;
}
}
// Special Patch for Square Resolution: Black Berry Passport
if (skip) {
double min = Math.min(mWidth, mHeight);
double max = Math.max(mWidth, mHeight);
if (max / min < 1.20) {
Log.v("SDL", "Don't skip on such aspect-ratio. Could be a square resolution.");
skip = false;
}
}
// Don't skip in MultiWindow.
if (skip) {
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) {
if (SDLActivity.mSingleton.isInMultiWindowMode()) {
Log.v("SDL", "Don't skip in Multi-Window");
skip = false;
}
}
}
if (skip) {
Log.v("SDL", "Skip .. Surface is not ready.");
mIsSurfaceReady = false;
return;
}
/* If the surface has been previously destroyed by onNativeSurfaceDestroyed, recreate it here */
SDLActivity.onNativeSurfaceChanged();
/* Surface is ready */
mIsSurfaceReady = true;
SDLActivity.mNextNativeState = SDLActivity.NativeState.RESUMED;
SDLActivity.handleNativeState();
}
// Key events
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
return SDLActivity.handleKeyEvent(v, keyCode, event, null);
}
// Touch events
@Override
public boolean onTouch(View v, MotionEvent event) {
/* Ref: http://developer.android.com/training/gestures/multi.html */
int touchDevId = event.getDeviceId();
final int pointerCount = event.getPointerCount();
int action = event.getActionMasked();
int pointerFingerId;
int i = -1;
float x,y,p;
/*
* Prevent id to be -1, since it's used in SDL internal for synthetic events
* Appears when using Android emulator, eg:
* adb shell input mouse tap 100 100
* adb shell input touchscreen tap 100 100
*/
if (touchDevId < 0) {
touchDevId -= 1;
}
// 12290 = Samsung DeX mode desktop mouse
// 12290 = 0x3002 = 0x2002 | 0x1002 = SOURCE_MOUSE | SOURCE_TOUCHSCREEN
// 0x2 = SOURCE_CLASS_POINTER
if (event.getSource() == InputDevice.SOURCE_MOUSE || event.getSource() == (InputDevice.SOURCE_MOUSE | InputDevice.SOURCE_TOUCHSCREEN)) {
int mouseButton = 1;
try {
Object object = event.getClass().getMethod("getButtonState").invoke(event);
if (object != null) {
mouseButton = (Integer) object;
}
} catch(Exception ignored) {
}
// We need to check if we're in relative mouse mode and get the axis offset rather than the x/y values
// if we are. We'll leverage our existing mouse motion listener
SDLGenericMotionListener_API12 motionListener = SDLActivity.getMotionListener();
x = motionListener.getEventX(event);
y = motionListener.getEventY(event);
SDLActivity.onNativeMouse(mouseButton, action, x, y, motionListener.inRelativeMode());
} else {
switch(action) {
case MotionEvent.ACTION_MOVE:
for (i = 0; i < pointerCount; i++) {
pointerFingerId = event.getPointerId(i);
x = event.getX(i) / mWidth;
y = event.getY(i) / mHeight;
p = event.getPressure(i);
if (p > 1.0f) {
// may be larger than 1.0f on some devices
// see the documentation of getPressure(i)
p = 1.0f;
}
SDLActivity.onNativeTouch(touchDevId, pointerFingerId, action, x, y, p);
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_DOWN:
// Primary pointer up/down, the index is always zero
i = 0;
/* fallthrough */
case MotionEvent.ACTION_POINTER_UP:
case MotionEvent.ACTION_POINTER_DOWN:
// Non primary pointer up/down
if (i == -1) {
i = event.getActionIndex();
}
pointerFingerId = event.getPointerId(i);
x = event.getX(i) / mWidth;
y = event.getY(i) / mHeight;
p = event.getPressure(i);
if (p > 1.0f) {
// may be larger than 1.0f on some devices
// see the documentation of getPressure(i)
p = 1.0f;
}
SDLActivity.onNativeTouch(touchDevId, pointerFingerId, action, x, y, p);
break;
case MotionEvent.ACTION_CANCEL:
for (i = 0; i < pointerCount; i++) {
pointerFingerId = event.getPointerId(i);
x = event.getX(i) / mWidth;
y = event.getY(i) / mHeight;
p = event.getPressure(i);
if (p > 1.0f) {
// may be larger than 1.0f on some devices
// see the documentation of getPressure(i)
p = 1.0f;
}
SDLActivity.onNativeTouch(touchDevId, pointerFingerId, MotionEvent.ACTION_UP, x, y, p);
}
break;
default:
break;
}
}
return true;
}
// Sensor events
public void enableSensor(int sensortype, boolean enabled) {
// TODO: This uses getDefaultSensor - what if we have >1 accels?
if (enabled) {
mSensorManager.registerListener(this,
mSensorManager.getDefaultSensor(sensortype),
SensorManager.SENSOR_DELAY_GAME, null);
} else {
mSensorManager.unregisterListener(this,
mSensorManager.getDefaultSensor(sensortype));
}
}
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
// TODO
}
@Override
public void onSensorChanged(SensorEvent event) {
if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
// Since we may have an orientation set, we won't receive onConfigurationChanged events.
// We thus should check here.
int newOrientation;
float x, y;
switch (mDisplay.getRotation()) {
case Surface.ROTATION_90:
x = -event.values[1];
y = event.values[0];
newOrientation = SDLActivity.SDL_ORIENTATION_LANDSCAPE;
break;
case Surface.ROTATION_270:
x = event.values[1];
y = -event.values[0];
newOrientation = SDLActivity.SDL_ORIENTATION_LANDSCAPE_FLIPPED;
break;
case Surface.ROTATION_180:
x = -event.values[0];
y = -event.values[1];
newOrientation = SDLActivity.SDL_ORIENTATION_PORTRAIT_FLIPPED;
break;
case Surface.ROTATION_0:
default:
x = event.values[0];
y = event.values[1];
newOrientation = SDLActivity.SDL_ORIENTATION_PORTRAIT;
break;
}
if (newOrientation != SDLActivity.mCurrentOrientation) {
SDLActivity.mCurrentOrientation = newOrientation;
SDLActivity.onNativeOrientationChanged(newOrientation);
}
SDLActivity.onNativeAccel(-x / SensorManager.GRAVITY_EARTH,
y / SensorManager.GRAVITY_EARTH,
event.values[2] / SensorManager.GRAVITY_EARTH);
}
}
// Captured pointer events for API 26.
public boolean onCapturedPointerEvent(MotionEvent event)
{
int action = event.getActionMasked();
float x, y;
switch (action) {
case MotionEvent.ACTION_SCROLL:
x = event.getAxisValue(MotionEvent.AXIS_HSCROLL, 0);
y = event.getAxisValue(MotionEvent.AXIS_VSCROLL, 0);
SDLActivity.onNativeMouse(0, action, x, y, false);
return true;
case MotionEvent.ACTION_HOVER_MOVE:
case MotionEvent.ACTION_MOVE:
x = event.getX(0);
y = event.getY(0);
SDLActivity.onNativeMouse(0, action, x, y, true);
return true;
case MotionEvent.ACTION_BUTTON_PRESS:
case MotionEvent.ACTION_BUTTON_RELEASE:
// Change our action value to what SDL's code expects.
if (action == MotionEvent.ACTION_BUTTON_PRESS) {
action = MotionEvent.ACTION_DOWN;
} else { /* MotionEvent.ACTION_BUTTON_RELEASE */
action = MotionEvent.ACTION_UP;
}
x = event.getX(0);
y = event.getY(0);
int button = event.getButtonState();
SDLActivity.onNativeMouse(button, action, x, y, true);
return true;
}
return false;
}
}
@@ -3,6 +3,9 @@
<item
android:id="@+id/install_from_zip"
android:title="@string/action_install_from_zip" />
<item
android:id="@+id/install_from_cd_image"
android:title="@string/action_install_from_cd_image" />
<item
android:id="@+id/export_savedata"
android:title="@string/action_export_save_data" />
@@ -18,4 +21,4 @@
<item
android:id="@+id/licenses"
android:title="@string/action_licenses" />
</menu>
</menu>
@@ -4,17 +4,24 @@
<string name="action_export_save_data">セーブデータをエクスポート</string>
<string name="action_help">使い方</string>
<string name="action_import_save_data">セーブデータをインポート</string>
<string name="action_install_from_cd_image">CDイメージからインストール</string>
<string name="action_install_from_zip">ZIPからインストール</string>
<string name="action_licenses">オープンソースライセンス</string>
<string name="action_clear_save_data">セーブデータをクリア</string>
<string name="cancel">キャンセル</string>
<string name="cannot_find_ald">System 3.x のファイル (*.ald) が見つかりません。</string>
<string name="cannot_find_game_data_directory">CDイメージ内に GAMEDATA ディレクトリが見つかりません。</string>
<string name="cd_image_read_error">選択された CD イメージファイルを読み取れません。</string>
<string name="choose_a_file">ファイルを選択</string>
<string name="choose_cd_image_files">CDイメージファイルを選択</string>
<string name="clear_save_data_confirm">セーブデータをすべて削除します。元には戻せません。本当によろしいですか?</string>
<string name="confirm">確認</string>
<string name="error_dialog_title">エラー</string>
<string name="install_dialog_title">インストール中…</string>
<string name="install_error">ゲームのインストールに失敗しました。</string>
<string name="install_progress">%s を展開中</string>
<string name="invalid_iso9660_image">有効な ISO9660 イメージではありません。</string>
<string name="missing_cd_image_metadata">CD イメージファイルと対応するメタデータファイルを一緒に選択してください。ファイル選択画面ではファイルを長押しすると複数選択モードに入れます。</string>
<string name="no_data_to_import">ZIP アーカイブ中に save/ フォルダが見つかりません。</string>
<string name="save_data_export_error">セーブデータのエクスポートに失敗しました。</string>
<string name="save_data_export_success">エクスポートに成功しました。</string>
@@ -22,6 +29,7 @@
<string name="save_data_clear_success">セーブデータをクリアしました。</string>
<string name="save_data_clear_error">セーブデータのクリアに失敗しました。</string>
<string name="uninstall_dialog_message">「%s」をアンインストールしますか?</string>
<string name="unsupported_cd_image">この CD イメージ形式にはまだ対応していません。現在は .iso、.bin/.img + .cue/.ccd、.mdf/.mds に対応しています。</string>
<string name="unsupported_zip">この形式の ZIP はサポートしていません。</string>
<string name="zip_extraction_error">ZIP の展開に失敗しました。</string>
<string name="usage">
@@ -41,4 +49,4 @@
<p>3. この画面の右上のメニューボタンをタップして「ZIPからインストール」を選択し、転送したZIPを選びます。</p>
]]>
</string>
</resources>
</resources>
@@ -4,16 +4,23 @@
<string name="action_export_save_data">Export Save Files</string>
<string name="action_help">Help</string>
<string name="action_import_save_data">Import Save Files</string>
<string name="action_install_from_cd_image">Install from CD image</string>
<string name="action_install_from_zip">Install from ZIP</string>
<string name="action_licenses">Open source licenses</string>
<string name="cancel">Cancel</string>
<string name="cannot_find_ald">Cannot find System 3.x game files (*.ald).</string>
<string name="cannot_find_game_data_directory">Cannot find GAMEDATA directory in the CD image.</string>
<string name="cd_image_read_error">Cannot read the selected CD image file.</string>
<string name="choose_a_file">Choose a file</string>
<string name="choose_cd_image_files">Choose CD image files</string>
<string name="clear_save_data_confirm">Are you sure you want to clear all save files? This cannot be undone.</string>
<string name="confirm">Confirm</string>
<string name="error_dialog_title">Error</string>
<string name="install_dialog_title">Installing…</string>
<string name="install_error">Failed to install game.</string>
<string name="install_progress">Extracting %s</string>
<string name="invalid_iso9660_image">This file is not a valid ISO9660 image.</string>
<string name="missing_cd_image_metadata">Select a matching metadata file with the CD image file. In the file picker, long-press a file to enter multi-select mode.</string>
<string name="no_data_to_import">No save/ folder in the ZIP archive.</string>
<string name="ok">OK</string>
<string name="save_data_clear_error">Failed to clear save files.</string>
@@ -22,6 +29,7 @@
<string name="save_data_import_success">Imported successfully.</string>
<string name="save_data_export_success">Exported successfully.</string>
<string name="uninstall_dialog_message">Uninstall \"%s\"?</string>
<string name="unsupported_cd_image">This CD image format is not supported yet. Currently only .iso, .bin/.img + .cue/.ccd, and .mdf/.mds are supported.</string>
<string name="unsupported_zip">This type of ZIP is not supported.</string>
<string name="zip_extraction_error">Failed to extract ZIP.</string>
<string name="usage">
@@ -0,0 +1,714 @@
/* Copyright (C) 2026 <KichikuouChrome@gmail.com>
*
* 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.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*
*/
package io.github.kichikuou.xsystem35
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertArrayEquals
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Assert.fail
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import java.io.ByteArrayOutputStream
import java.io.File
import java.nio.charset.Charset
class Iso9660FileSystemTest {
@get:Rule
val tmp = TemporaryFolder()
@Test
fun extractsGameDataDirectoryFromPrimaryDescriptor() = runBlocking {
val iso = SyntheticIsoBuilder()
.addGameDataFile("SA.ALD;1", "ald".toByteArray())
.addGameDataFile("SUB/FILE.TXT;1", "text".toByteArray())
.writeTo(tmp.newFile("game.iso"))
CdImageReader.openForFile(iso).use { reader ->
val fs = Iso9660FileSystem(reader)
val gameData = fs.findGameDataDirectory()
?: throw AssertionError("GAMEDATA directory was not found")
val extracted = mutableMapOf<String, ByteArray>()
fs.extractDirectory(gameData, "GAMEDATA") { path, input ->
extracted[path] = input.readBytes()
}
assertArrayEquals("ald".toByteArray(), extracted["GAMEDATA/SA.ALD"])
assertArrayEquals("text".toByteArray(), extracted["GAMEDATA/SUB/FILE.TXT"])
}
}
@Test
fun decodesPrimaryDescriptorNamesAsShiftJis() = runBlocking {
val name = "表.TXT;1"
val iso = SyntheticIsoBuilder()
.addGameDataFile(name, "sjis".toByteArray())
.writeTo(tmp.newFile("sjis.iso"))
CdImageReader.openForFile(iso).use { reader ->
val fs = Iso9660FileSystem(reader)
val gameData = fs.findGameDataDirectory()
?: throw AssertionError("GAMEDATA directory was not found")
val extracted = mutableMapOf<String, ByteArray>()
fs.extractDirectory(gameData, "GAMEDATA") { path, input ->
extracted[path] = input.readBytes()
}
assertArrayEquals("sjis".toByteArray(), extracted["GAMEDATA/表.TXT"])
}
}
@Test
fun prefersJolietDescriptorOverPrimaryDescriptor() = runBlocking {
val iso = SyntheticIsoBuilder()
.addGameDataFile("PRIMARY.TXT;1", "primary".toByteArray())
.addJolietGameDataFile("日本語.TXT;1", "joliet".toByteArray())
.writeTo(tmp.newFile("joliet.iso"))
CdImageReader.openForFile(iso).use { reader ->
val fs = Iso9660FileSystem(reader)
val gameData = fs.findGameDataDirectory()
?: throw AssertionError("GAMEDATA directory was not found")
val extracted = mutableMapOf<String, ByteArray>()
fs.extractDirectory(gameData, "GAMEDATA") { path, input ->
extracted[path] = input.readBytes()
}
assertFalse(extracted.containsKey("GAMEDATA/PRIMARY.TXT"))
assertArrayEquals("joliet".toByteArray(), extracted["GAMEDATA/日本語.TXT"])
}
}
@Test
fun rejectsInvalidVolumeDescriptorSignature() {
val iso = tmp.newFile("invalid.iso")
iso.writeBytes(ByteArray(18 * CdImageReader.ISO_SECTOR_SIZE))
val e = assertInstallFailure(R.string.invalid_iso9660_image) {
CdImageReader.openForFile(iso).use { Iso9660FileSystem(it) }
}
assertEquals(R.string.invalid_iso9660_image, e.msgId)
}
}
class CdImageReaderTest {
@get:Rule
val tmp = TemporaryFolder()
@Test
fun readsIsoSectorAt2048ByteOffset() {
val sector0 = sectorFilledWith(0x10)
val sector1 = sectorFilledWith(0x20)
val image = tmp.newFile("plain.iso")
image.writeBytes(sector0 + sector1)
CdImageReader.openForFile(image).use { reader ->
val buffer = ByteArray(CdImageReader.ISO_SECTOR_SIZE)
reader.readSector(1, buffer)
assertArrayEquals(sector1, buffer)
assertTrue(reader.audioTracks().isEmpty())
}
}
@Test
fun cueMode12048ReadsDataSector() {
val payload = sectorFilledWith(0x31)
val image = tmp.newFile("mode2048.bin")
image.writeBytes(payload)
CdImageReader.openForFile(image, "mode2048.cue", cue("MODE1/2048").toByteArray()).use { reader ->
val buffer = ByteArray(CdImageReader.ISO_SECTOR_SIZE)
reader.readSector(0, buffer)
assertArrayEquals(payload, buffer)
}
}
@Test
fun cueMode12352ReadsPayloadAtOffset16() {
val payload = sectorFilledWith(0x42)
val image = tmp.newFile("mode2352.bin")
image.writeBytes(rawDataSector(payload, 0x11))
CdImageReader.openForFile(image, "mode2352.cue", cue("MODE1/2352").toByteArray()).use { reader ->
val buffer = ByteArray(CdImageReader.ISO_SECTOR_SIZE)
reader.readSector(0, buffer)
assertArrayEquals(payload, buffer)
}
}
@Test
fun cuePregapAdvancesDataTrackStart() {
val pregapPayload = sectorFilledWith(0x21)
val dataPayload = sectorFilledWith(0x22)
val image = tmp.newFile("pregap.bin")
image.writeBytes(rawDataSector(pregapPayload, 0x01) + rawDataSector(dataPayload, 0x02))
val metadata = """
FILE "pregap.bin" BINARY
TRACK 01 MODE1/2352
INDEX 00 00:00:00
INDEX 01 00:00:01
""".trimIndent()
CdImageReader.openForFile(image, "pregap.cue", metadata.toByteArray()).use { reader ->
val buffer = ByteArray(CdImageReader.ISO_SECTOR_SIZE)
reader.readSector(1, buffer)
assertArrayEquals(dataPayload, buffer)
}
}
@Test
fun extractsCueAudioTrackAsWav() {
val dataPayload = sectorFilledWith(0x33)
val audio = ByteArray(RAW_AUDIO_SECTOR_SIZE) { it.toByte() }
val image = tmp.newFile("audio.bin")
image.writeBytes(rawDataSector(dataPayload, 0) + audio)
val metadata = """
FILE "audio.bin" BINARY
TRACK 01 MODE1/2352
INDEX 01 00:00:00
TRACK 02 AUDIO
INDEX 01 00:00:01
""".trimIndent()
CdImageReader.openForFile(image, "audio.cue", metadata.toByteArray()).use { reader ->
val output = ByteArrayOutputStream()
assertEquals(listOf(2), reader.audioTracks())
reader.extractAudioTrack(2, output)
val wav = output.toByteArray()
assertEquals("RIFF", wav.decodeAscii(0, 4))
assertEquals(RAW_AUDIO_SECTOR_SIZE + 36, wav.readLeInt(4))
assertEquals("WAVE", wav.decodeAscii(8, 4))
assertEquals("data", wav.decodeAscii(36, 4))
assertEquals(RAW_AUDIO_SECTOR_SIZE, wav.readLeInt(40))
assertEquals(44100, wav.readLeInt(24))
assertEquals(2, wav.readLeShort(22))
assertEquals(16, wav.readLeShort(34))
assertArrayEquals(audio, wav.copyOfRange(44, 44 + RAW_AUDIO_SECTOR_SIZE))
}
}
@Test
fun rejectsUnsupportedCueMode() {
val image = tmp.newFile("unsupported.bin")
image.writeBytes(ByteArray(CdImageReader.ISO_SECTOR_SIZE))
assertInstallFailure(R.string.unsupported_cd_image) {
CdImageReader.openForFile(image, "unsupported.cue", cue("MODE2/2352").toByteArray())
}
}
@Test
fun ccdUsesMode0ForAudioAndMode1PayloadOffsetForData() {
val dataPayload = sectorFilledWith(0x52)
val audio = ByteArray(RAW_AUDIO_SECTOR_SIZE) { (255 - it).toByte() }
val image = tmp.newFile("disc.img")
image.writeBytes(rawDataSector(dataPayload, 0x08) + audio)
val metadata = """
[TRACK 1]
MODE=1
INDEX 1=0
[TRACK 2]
MODE=0
INDEX 1=1
""".trimIndent()
CdImageReader.openForFile(image, "disc.ccd", metadata.toByteArray()).use { reader ->
val buffer = ByteArray(CdImageReader.ISO_SECTOR_SIZE)
val output = ByteArrayOutputStream()
reader.readSector(0, buffer)
reader.extractAudioTrack(2, output)
assertArrayEquals(dataPayload, buffer)
assertEquals(listOf(2), reader.audioTracks())
assertArrayEquals(audio, output.toByteArray().copyOfRange(44, 44 + RAW_AUDIO_SECTOR_SIZE))
}
}
@Test
fun mdsMapsModeAAToDataAndModeA9ToAudio() {
val dataPayload = sectorFilledWith(0x61)
val audio = ByteArray(RAW_AUDIO_SECTOR_SIZE) { (it * 3).toByte() }
val image = tmp.newFile("disc.mdf")
image.writeBytes(rawDataSector(dataPayload, 0x09) + audio)
CdImageReader.openForFile(image, "disc.mds", mds(track1Sectors = 1, track2Sectors = 1)).use { reader ->
val buffer = ByteArray(CdImageReader.ISO_SECTOR_SIZE)
val output = ByteArrayOutputStream()
reader.readSector(0, buffer)
reader.extractAudioTrack(2, output)
assertArrayEquals(dataPayload, buffer)
assertEquals(listOf(2), reader.audioTracks())
assertArrayEquals(audio, output.toByteArray().copyOfRange(44, 44 + RAW_AUDIO_SECTOR_SIZE))
}
}
@Test
fun rejectsMdsWithoutMediaDescriptorSignature() {
val image = tmp.newFile("bad.mdf")
image.writeBytes(ByteArray(RAW_AUDIO_SECTOR_SIZE))
assertInstallFailure(R.string.unsupported_cd_image) {
CdImageReader.openForFile(image, "bad.mds", ByteArray(0x70))
}
}
}
class GameInstallerCdImageTest {
@get:Rule
val tmp = TemporaryFolder()
@Test
fun installsDataOnlyIso() = runBlocking {
val root = tmp.newFolder("root")
val iso = SyntheticIsoBuilder()
.addGameDataFile("SA.ALD;1", "ald".toByteArray())
.addGameDataFile("TITLE.TXT;1", "title".toByteArray())
.writeTo(tmp.newFile("game.iso"))
CdImageReader.openForFile(iso).use { reader ->
val gameDir = GameInstaller(GameStore(root)).installCdImageForTest(reader) {}
assertEquals(File(root, "0/GAMEDATA").canonicalFile, gameDir.canonicalFile)
assertEquals("GAMEDATA", File(root, "0/${Launcher.GAMEDIR_FILE}").readText())
assertEquals("ald", File(gameDir, "SA.ALD").readText())
assertEquals("title", File(gameDir, "TITLE.TXT").readText())
}
}
@Test
fun installsCueAudioTrackAndPlaylist() = runBlocking {
val root = tmp.newFolder("root")
val isoSector = SyntheticIsoBuilder()
.addGameDataFile("SA.ALD;1", "ald".toByteArray())
.toByteArray()
val dataSectors = isoSector.toList().chunked(CdImageReader.ISO_SECTOR_SIZE)
.joinToByteArray { rawDataSector(it.toByteArray(), 0) }
val audio = ByteArray(RAW_AUDIO_SECTOR_SIZE) { (it and 0x7f).toByte() }
val image = tmp.newFile("audio.bin")
image.writeBytes(dataSectors + audio)
val dataSectorCount = isoSector.size / CdImageReader.ISO_SECTOR_SIZE
val metadata = """
FILE "audio.bin" BINARY
TRACK 01 MODE1/2352
INDEX 01 00:00:00
TRACK 02 AUDIO
INDEX 01 ${dataSectorCount.toMsf()}
""".trimIndent()
CdImageReader.openForFile(image, "audio.cue", metadata.toByteArray()).use { reader ->
val gameDir = GameInstaller(GameStore(root)).installCdImageForTest(reader) {}
val wav = File(gameDir, "cdda/track02.wav")
assertTrue(wav.exists())
assertEquals("\ncdda/track02.wav", File(gameDir, Launcher.PLAYLIST_FILE).readText())
}
}
@Test
fun rejectsIsoWithoutGameDataDirectoryAndDeletesIncompleteInstall() = runBlocking {
val root = tmp.newFolder("root")
val iso = SyntheticIsoBuilder(includeGameData = false).writeTo(tmp.newFile("nogamedata.iso"))
CdImageReader.openForFile(iso).use { reader ->
assertInstallFailure(R.string.cannot_find_game_data_directory) {
runBlocking {
GameInstaller(GameStore(root)).installCdImageForTest(reader) {}
}
}
}
assertFalse(File(root, "0").exists())
}
@Test
fun rejectsIsoWithoutAldAndDeletesIncompleteInstall() = runBlocking {
val root = tmp.newFolder("root")
val iso = SyntheticIsoBuilder()
.addGameDataFile("README.TXT;1", "readme".toByteArray())
.writeTo(tmp.newFile("noald.iso"))
CdImageReader.openForFile(iso).use { reader ->
assertInstallFailure(R.string.cannot_find_ald) {
runBlocking {
GameInstaller(GameStore(root)).installCdImageForTest(reader) {}
}
}
}
assertFalse(File(root, "0").exists())
}
@Test
fun resolveOutputPathRejectsTraversal() {
val base = tmp.newFolder("out")
assertThrowsIOException {
resolveOutputPath(base, "../evil")
}
assertThrowsIOException {
resolveOutputPath(base, File(base.parentFile, "evil").absolutePath)
}
}
}
private class SyntheticIsoBuilder(
private val includeGameData: Boolean = true,
) {
private val primaryFiles = mutableMapOf<String, ByteArray>()
private val jolietFiles = mutableMapOf<String, ByteArray>()
fun addGameDataFile(path: String, content: ByteArray): SyntheticIsoBuilder {
primaryFiles[path] = content
return this
}
fun addJolietGameDataFile(path: String, content: ByteArray): SyntheticIsoBuilder {
jolietFiles[path] = content
return this
}
fun writeTo(file: File): File {
file.writeBytes(toByteArray())
return file
}
fun toByteArray(): ByteArray {
val sectors = MutableList(80) { ByteArray(CdImageReader.ISO_SECTOR_SIZE) }
var nextSector = 30
val primaryTree = buildTree(primaryFiles, Charset.forName("Shift_JIS"))
val primaryRoot = writeTree(sectors, primaryTree, ROOT_SECTOR, GAMEDATA_SECTOR, nextSector, false)
nextSector = primaryRoot.nextSector
writeVolumeDescriptor(sectors[16], 1, ROOT_SECTOR, CdImageReader.ISO_SECTOR_SIZE, false)
var terminatorSector = 17
if (jolietFiles.isNotEmpty()) {
val jolietTree = buildTree(jolietFiles, Charsets.UTF_16BE)
writeTree(sectors, jolietTree, JOLIET_ROOT_SECTOR, JOLIET_GAMEDATA_SECTOR, nextSector, true)
writeVolumeDescriptor(sectors[17], 2, JOLIET_ROOT_SECTOR, CdImageReader.ISO_SECTOR_SIZE, true)
terminatorSector = 18
}
writeTerminator(sectors[terminatorSector])
return sectors.flattenToByteArray()
}
private fun buildTree(files: Map<String, ByteArray>, charset: Charset): DirectoryNode {
val root = DirectoryNode("")
if (!includeGameData) {
return root
}
val gameData = DirectoryNode("GAMEDATA")
root.directories["GAMEDATA"] = gameData
for ((path, content) in files) {
var directory = gameData
val parts = path.split("/")
for (part in parts.dropLast(1)) {
directory = directory.directories.getOrPut(part.substringBefore(";")) {
DirectoryNode(part.substringBefore(";"))
}
}
val rawName = parts.last().toByteArray(charset)
directory.files.add(FileNode(rawName, content))
}
return root
}
private fun writeTree(
sectors: MutableList<ByteArray>,
root: DirectoryNode,
rootSector: Int,
gameDataSector: Int,
firstFileSector: Int,
joliet: Boolean,
): TreeWriteResult {
var nextSector = firstFileSector
root.extent = rootSector
root.size = CdImageReader.ISO_SECTOR_SIZE
if (includeGameData) {
root.directories["GAMEDATA"]!!.extent = gameDataSector
root.directories["GAMEDATA"]!!.size = CdImageReader.ISO_SECTOR_SIZE
}
assignDirectorySectors(root, gameDataSector + 1)
nextSector = writeFileContents(sectors, root, nextSector)
writeDirectorySector(sectors[rootSector], root, root, joliet)
if (includeGameData) {
writeDirectorySectors(sectors, root.directories["GAMEDATA"]!!, root, joliet)
}
return TreeWriteResult(nextSector)
}
private fun assignDirectorySectors(directory: DirectoryNode, nextDirectorySector: Int): Int {
var sector = nextDirectorySector
for (child in directory.directories.values) {
if (child.name == "GAMEDATA") {
sector = assignDirectorySectors(child, sector)
continue
}
child.extent = sector++
child.size = CdImageReader.ISO_SECTOR_SIZE
sector = assignDirectorySectors(child, sector)
}
return sector
}
private fun writeFileContents(
sectors: MutableList<ByteArray>,
directory: DirectoryNode,
firstFileSector: Int,
): Int {
var sector = firstFileSector
for (child in directory.directories.values) {
sector = writeFileContents(sectors, child, sector)
}
for (file in directory.files) {
file.extent = sector
file.size = file.content.size
val sectorCount = (file.content.size + CdImageReader.ISO_SECTOR_SIZE - 1) /
CdImageReader.ISO_SECTOR_SIZE
for (i in 0 until sectorCount) {
file.content.copyInto(
sectors[sector + i],
0,
i * CdImageReader.ISO_SECTOR_SIZE,
minOf(file.content.size, (i + 1) * CdImageReader.ISO_SECTOR_SIZE)
)
}
sector += maxOf(1, sectorCount)
}
return sector
}
private fun writeDirectorySectors(
sectors: MutableList<ByteArray>,
directory: DirectoryNode,
parent: DirectoryNode,
joliet: Boolean,
) {
writeDirectorySector(sectors[directory.extent], directory, parent, joliet)
for (child in directory.directories.values) {
writeDirectorySectors(sectors, child, directory, joliet)
}
}
private fun writeDirectorySector(
sector: ByteArray,
directory: DirectoryNode,
parent: DirectoryNode,
joliet: Boolean,
) {
var offset = 0
offset += writeRecord(sector, offset, byteArrayOf(0), directory.extent, directory.size, true)
offset += writeRecord(sector, offset, byteArrayOf(1), parent.extent, parent.size, true)
for (child in directory.directories.values) {
val name = if (joliet) child.name.toByteArray(Charsets.UTF_16BE) else child.name.toByteArray()
offset += writeRecord(sector, offset, name, child.extent, child.size, true)
}
for (file in directory.files) {
offset += writeRecord(sector, offset, file.rawName, file.extent, file.size, false)
}
}
private fun writeVolumeDescriptor(
sector: ByteArray,
type: Int,
rootExtent: Int,
rootSize: Int,
joliet: Boolean,
) {
sector[0] = type.toByte()
"CD001".toByteArray(Charsets.US_ASCII).copyInto(sector, 1)
sector[6] = 1
if (joliet) {
sector[88] = 0x25
sector[89] = 0x2f
sector[90] = 0x45
}
writeRecord(sector, 156, byteArrayOf(0), rootExtent, rootSize, true)
}
private fun writeTerminator(sector: ByteArray) {
sector[0] = 255.toByte()
"CD001".toByteArray(Charsets.US_ASCII).copyInto(sector, 1)
sector[6] = 1
}
private fun writeRecord(
buffer: ByteArray,
offset: Int,
rawName: ByteArray,
extent: Int,
size: Int,
isDirectory: Boolean,
): Int {
val length = 33 + rawName.size + if (rawName.size % 2 == 0) 1 else 0
buffer[offset] = length.toByte()
buffer.writeLeInt(offset + 2, extent)
buffer.writeLeInt(offset + 10, size)
buffer[offset + 25] = if (isDirectory) 0x02 else 0x00
buffer[offset + 32] = rawName.size.toByte()
rawName.copyInto(buffer, offset + 33)
return length
}
companion object {
private const val ROOT_SECTOR = 20
private const val GAMEDATA_SECTOR = 21
private const val JOLIET_ROOT_SECTOR = 24
private const val JOLIET_GAMEDATA_SECTOR = 25
}
}
private data class DirectoryNode(
val name: String,
val directories: MutableMap<String, DirectoryNode> = linkedMapOf(),
val files: MutableList<FileNode> = mutableListOf(),
var extent: Int = 0,
var size: Int = 0,
)
private data class FileNode(
val rawName: ByteArray,
val content: ByteArray,
var extent: Int = 0,
var size: Int = 0,
)
private data class TreeWriteResult(val nextSector: Int)
private const val RAW_AUDIO_SECTOR_SIZE = 2352
private fun cue(mode: String): String = """
FILE "image.bin" BINARY
TRACK 01 $mode
INDEX 01 00:00:00
""".trimIndent()
private fun mds(track1Sectors: Int, track2Sectors: Int): ByteArray {
val bytes = ByteArray(0x70 + 2 * 0x50 + 2 * 8)
"MEDIA DESCRIPTOR".toByteArray(Charsets.US_ASCII).copyInto(bytes, 0)
bytes[0x62] = 2
val track1 = 0x70
bytes[track1] = 0xaa.toByte()
bytes[track1 + 0x04] = 1
bytes.writeLeShort(track1 + 0x10, RAW_AUDIO_SECTOR_SIZE)
bytes.writeLeInt(track1 + 0x28, 0)
bytes.writeLeInt(0x70 + 2 * 0x50 + 0x04, track1Sectors)
val track2 = 0x70 + 0x50
bytes[track2] = 0xa9.toByte()
bytes[track2 + 0x04] = 2
bytes.writeLeShort(track2 + 0x10, RAW_AUDIO_SECTOR_SIZE)
bytes.writeLeInt(track2 + 0x28, track1Sectors * RAW_AUDIO_SECTOR_SIZE)
bytes.writeLeInt(0x70 + 2 * 0x50 + 8 + 0x04, track2Sectors)
return bytes
}
private fun sectorFilledWith(value: Int): ByteArray {
return ByteArray(CdImageReader.ISO_SECTOR_SIZE) { value.toByte() }
}
private fun rawDataSector(payload: ByteArray, prefixValue: Int): ByteArray {
val sector = ByteArray(RAW_AUDIO_SECTOR_SIZE) { prefixValue.toByte() }
payload.copyInto(sector, 16, 0, CdImageReader.ISO_SECTOR_SIZE)
return sector
}
private fun Int.toMsf(): String {
val minutes = this / (60 * 75)
val seconds = this / 75 % 60
val frames = this % 75
return "%02d:%02d:%02d".format(minutes, seconds, frames)
}
private fun Iterable<Byte>.toByteArray(): ByteArray {
val result = ByteArray(count())
forEachIndexed { index, byte -> result[index] = byte }
return result
}
private fun List<ByteArray>.flattenToByteArray(): ByteArray {
val result = ByteArray(sumOf { it.size })
var offset = 0
for (bytes in this) {
bytes.copyInto(result, offset)
offset += bytes.size
}
return result
}
private fun List<List<Byte>>.joinToByteArray(transform: (List<Byte>) -> ByteArray): ByteArray {
return map(transform).flattenToByteArray()
}
private fun ByteArray.decodeAscii(offset: Int, length: Int): String {
return String(this, offset, length, Charsets.US_ASCII)
}
private fun ByteArray.readLeShort(offset: Int): Int {
return (this[offset].toInt() and 0xff) or ((this[offset + 1].toInt() and 0xff) shl 8)
}
private fun ByteArray.readLeInt(offset: Int): Int {
return readLeShort(offset) or (readLeShort(offset + 2) shl 16)
}
private fun ByteArray.writeLeShort(offset: Int, value: Int) {
this[offset] = value.toByte()
this[offset + 1] = (value shr 8).toByte()
}
private fun ByteArray.writeLeInt(offset: Int, value: Int) {
writeLeShort(offset, value)
writeLeShort(offset + 2, value shr 16)
}
private fun assertInstallFailure(expectedMsgId: Int, block: () -> Unit): InstallFailureException {
try {
block()
} catch (e: InstallFailureException) {
assertEquals(expectedMsgId, e.msgId)
return e
}
fail("Expected InstallFailureException")
throw AssertionError()
}
private fun assertThrowsIOException(block: () -> Unit) {
try {
block()
} catch (e: java.io.IOException) {
return
}
fail("Expected IOException")
}
+4 -4
View File
@@ -1,17 +1,17 @@
#cmakedefine PACKAGE "@PACKAGE@"
#define VERSION "@XSYSTEM35_VERSION@"
#cmakedefine CACHE_TOTALSIZE @CACHE_TOTALSIZE@
#ifndef XSYSTEM35_SDL_VERSION
#define XSYSTEM35_SDL_VERSION @XSYSTEM35_SDL_VERSION@
#endif
#cmakedefine ENABLE_DEBUGGER @ENABLE_DEBUGGER@
#cmakedefine ENABLE_GTK @ENABLE_GTK@
#cmakedefine ENABLE_MIDI_SDLMIXER @ENABLE_MIDI_SDLMIXER@
#cmakedefine ENABLE_MIDI_PORTMIDI @ENABLE_MIDI_PORTMIDI@
#cmakedefine ENABLE_NLS @ENABLE_NLS@
#cmakedefine DEFAULT_PLAYLIST_PATH "@DEFAULT_PLAYLIST_PATH@"
#cmakedefine JOY_DEVICE "@JOY_DEVICE@"
#cmakedefine HAVE_LIBINTL @HAVE_LIBINTL@
#cmakedefine HAVE_GETLOGIN @HAVE_GETLOGIN@
#cmakedefine HAVE_MMAP @HAVE_MMAP@
#cmakedefine HAVE_SIGACTION @HAVE_SIGACTION@
+3
View File
@@ -98,6 +98,9 @@ finds System 3.x game files (*.ALD) from the current directory.
*-noimagecursor*::
Disable custom mouse cursor images.
*-mute_on_unfocus*::
Mute audio while the game window is not in focus.
*-version*::
Print the version number and exit.
+19
View File
@@ -0,0 +1,19 @@
Copyright (c) 2024 rxi
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+2 -2
View File
@@ -12,8 +12,8 @@ or in your `.xsys35rc` file. For example:
xsystem35 -censor misc/censor/kichikuou.txt
```
The list files contain image numbers (integers), one per line. Lines starting
with `#` are treated as comments.
The list files contain image numbers (integers), one per line. A `#` starts a
comment that continues to the end of the line.
## Contributing
+30
View File
@@ -0,0 +1,30 @@
# Censor list for ランス -光をもとめて- (Rance: The Quest for Hikari)
# Images marked with a * might be allowed in an 18+ stream.
5
6
8
22
26
27
29
32
39
40
41
51
56
57
75
76
77
78 # *
80
82
89
91
96 # *
97 # *
98
99
101
208
+59
View File
@@ -0,0 +1,59 @@
# Censor list for ランスII (Rance II)
# Images marked with a * might be allowed in an 18+ stream.
26
39 # *
40
41
77
78
86
98
103
104
121
122
123
124
130
131
132
133
134
140
141
142
148
156
157
158
159
161
166
171
172
185
187
191
195
197
200
201
234
236
245
246
247
272
274
275
276
278
280
281
282
284
289
292
299
300
302
+121
View File
@@ -0,0 +1,121 @@
# Censor list for Rance 3
# Images marked with a * might be allowed in an 18+ stream.
1 # *
2 # *
3
6
8
9
32 # *
53 # *
54
70
71
72
73
75
77 # *
78
79
112 # *
113
114
115
116
117
118 # *
150
151
152
153
154
155
166
167
180
181
183
184
185
186
187
188
189
190
195
196
208
209
210
211
212
213
215
216
217
218
219
231
232
233
234
235
236
238
239
260
261
263
264
265
266
268
269
330
331
332
334
335
336
337
338
339
420
421
422
423
424
425
426
427
428
429
440
441
442
443
444
445
447
473
474
475
476
477
479
519 # *
521 # *
523
524
525
526
530
533 # *
534
535
536
537
570
571
572
581
+168
View File
@@ -0,0 +1,168 @@
# Censor list for Rance IV
# Images marked with a * might be allowed in an 18+ stream.
1
2
10
20
26
28
29
30 # *
31
32
33
34
35
36
37
38
39 # *
40
41
42
43
44
45
46
48
49
75
76
77
78
79
122
126
140
142
148
149
162 # *
163
164
165 # *
166
167
168
169 # *
170
171
172
176
177
178
185
186
188
189
205
207
217 # *
220
221
222
223
228
241
242
251
252
253
254
260
261
262 # *
264 # *
265
266
267
268
269
280
281
283
284
285
286 # *
287
288
292
294
299
370
371
372
373
376
378
382
384
385 # *
386
387
388
389
414 # *
416
428 # *
531 # *
532
533
534 # *
535
550
551
552
553
554
555
562 # *
565
615
625
626
711
723
1055
1056
1057
1058
1059
1060
1061
1062
1545 # *
1546 # *
1547 # *
1548 # *
1549 # *
1653 # *
1654 # *
1655 # *
1656 # *
1657 # *
1658 # *
1659 # *
1660 # *
1661 # *
1662 # *
1663 # *
1664 # *
1665 # *
1728
1729
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
+1 -1
View File
@@ -12,7 +12,7 @@ static void Init() {
int p1 = getCaliValue(); /* ISys3x */
int p2 = getCaliValue(); /* IWinMsg */
int p3 = getCaliValue(); /* ITimer */
int *var = getCaliVariable();
vmvar_t *var = getCaliVariable();
*var = 1;
+3 -3
View File
@@ -84,7 +84,7 @@ endif()
target_compile_options(modules PRIVATE -Wno-pointer-sign)
target_link_libraries(modules PRIVATE sdl2 zlib)
if (TARGET sdl2_mixer)
target_link_libraries(modules PRIVATE sdl2_mixer)
target_link_libraries(modules PRIVATE xsystem35_sdl zlib)
if (TARGET xsystem35_sdl_mixer)
target_link_libraries(modules PRIVATE xsystem35_sdl_mixer)
endif()
+6 -6
View File
@@ -15,8 +15,8 @@ static void Init() {
static void ExistKeyFile() {
const char *p1 = sl_getString(0);
int *p2 = getCaliVariable();
int *var = getCaliVariable();
vmvar_t *p2 = getCaliVariable();
vmvar_t *var = getCaliVariable();
*var = 1;
@@ -25,8 +25,8 @@ static void ExistKeyFile() {
static void CheckProtectFile() {
const char *p1 = sl_getString(0);
int *p2 = getCaliVariable();
int *var = getCaliVariable();
vmvar_t *p2 = getCaliVariable();
vmvar_t *var = getCaliVariable();
*var = 1;
@@ -35,8 +35,8 @@ static void CheckProtectFile() {
static void CreateKeyFile() {
const char *p1 = sl_getString(0);
int *p2 = getCaliVariable();
int *var = getCaliVariable();
vmvar_t *p2 = getCaliVariable();
vmvar_t *var = getCaliVariable();
*var = 1;
+13 -13
View File
@@ -135,7 +135,7 @@ static void Create() {
height: surface の高さ
bpp : surface の深さ(24bppのみサポート)
*/
int *var = getCaliVariable();
vmvar_t *var = getCaliVariable();
int width = getCaliValue();
int height = getCaliValue();
int bpp = getCaliValue();
@@ -162,7 +162,7 @@ static void CreatePixelOnly() {
height: surface の高さ
bpp : surface の深さ(24bpp only)
*/
int *var = getCaliVariable();
vmvar_t *var = getCaliVariable();
int width = getCaliValue();
int height = getCaliValue();
int bpp = getCaliValue();
@@ -188,7 +188,7 @@ static void CreateAMapOnly() {
width : surface の幅
height: surface の高さ
*/
int *var = getCaliVariable();
vmvar_t *var = getCaliVariable();
int width = getCaliValue();
int height = getCaliValue();
surface_t *s;
@@ -214,7 +214,7 @@ static void IsSurface() {
var : 結果を返す変数。surface ならば 1, !surface ならば 0
*/
int p1 = getCaliValue();
int *var = getCaliVariable();
vmvar_t *var = getCaliVariable();
*var = sf_get(p1) ? 1 : 0;
@@ -229,7 +229,7 @@ static void IsPixel() {
var : 結果を返す変数。pixel ならば 1, !pixel ならば 0
*/
int p1 = getCaliValue();
int *var = getCaliVariable();
vmvar_t *var = getCaliVariable();
surface_t *s;
s = sf_get(p1);
@@ -251,7 +251,7 @@ static void IsAlpha() {
var : 結果を返す変数。alpha ならば 1, !alpha ならば 0
*/
int p1 = getCaliValue();
int *var = getCaliVariable();
vmvar_t *var = getCaliVariable();
surface_t *s;
s = sf_get(p1);
@@ -273,7 +273,7 @@ static void GetWidth() {
var : 結果を返す変数。
*/
int p1 = getCaliValue();
int *var = getCaliVariable();
vmvar_t *var = getCaliVariable();
surface_t *s;
s = sf_get(p1);
@@ -295,7 +295,7 @@ static void GetHeight() {
var : 結果を返す変数。
*/
int p1 = getCaliValue();
int *var = getCaliVariable();
vmvar_t *var = getCaliVariable();
surface_t *s;
s = sf_get(p1);
@@ -310,7 +310,7 @@ static void GetHeight() {
}
static void GetCreatedSurface() { /* not used ? */
int *var = getCaliVariable();
vmvar_t *var = getCaliVariable();
TRACE_UNIMPLEMENTED("Gpx.GetCreatedSurface %p:", var);
}
@@ -322,7 +322,7 @@ static void LoadCG() {
var : 作成した surface の番号を返す変数
p1 : 読み込む CG の番号
*/
int *var = getCaliVariable();
vmvar_t *var = getCaliVariable();
int p1 = getCaliValue();
*var = load_cg_main(p1 -1);
@@ -331,14 +331,14 @@ static void LoadCG() {
}
static void GetCGPosX() { /* not useed ? */
int *var = getCaliVariable();
vmvar_t *var = getCaliVariable();
int p1 = getCaliValue();
TRACE_UNIMPLEMENTED("Gpx.GetCgPosX %p,%d:", var, p1);
}
static void GetCGPosY() { /* not useed ? */
int *var = getCaliVariable();
vmvar_t *var = getCaliVariable();
int p1 = getCaliValue();
TRACE_UNIMPLEMENTED("Gpx.GetCgPosY %p,%d:", var, p1);
@@ -1117,7 +1117,7 @@ static void EffectCopy() {
int sw = getCaliValue();
int sh = getCaliValue();
int time = getCaliValue();
int *var = getCaliVariable();
vmvar_t *var = getCaliVariable();
TRACE("Gpx.EffectCopy %d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%p:", no, dx, dy, ss1, sx1, sy1, ss2, sx2, sy2, sw, sh, time, var);
+1 -1
View File
@@ -17,7 +17,7 @@ void gpx_effect(int no,
surface_t *src, int sx, int sy,
int width, int height,
int time,
int *endtype) {
vmvar_t *endtype) {
surface_t *write = nact->ags.dib;
if (!gr_clip(dst, &dx, &dy, &width, &height, write, &wx, &wy)) return;
if (!gr_clip(src, &sx, &sy, &width, &height, write, &wx, &wy)) return;
+2 -1
View File
@@ -1,6 +1,7 @@
#ifndef __GLEFFECTCOPY_H__
#define __GLEFFECTCOPY_H__
#include "portab.h"
#include "surface.h"
extern void gpx_effect(int no,
@@ -9,6 +10,6 @@ extern void gpx_effect(int no,
surface_t *src, int sx, int sy,
int width, int height,
int time,
int *endtype);
vmvar_t *endtype);
#endif /* __GLEFFECTCOPY_H__ */
+9 -8
View File
@@ -3,7 +3,7 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <SDL.h>
#include "sdl_compat.h"
#include "portab.h"
#include "system.h"
@@ -76,12 +76,13 @@ void gr_copy(surface_t *dst, int dx, int dy, surface_t *src, int sx, int sy, int
SDL_Rect r;
if (SDL_IntersectRect(&src_rect, &dst_rect, &r)) {
SDL_Surface *view = gfx_createSurfaceView(src->sdl_surface, sx, sy, sw, sh);
SDL_Surface *tmp = SDL_ConvertSurface(view, dst->sdl_surface->format, 0);
SDL_Surface *tmp = sdl_convert_surface(
view, sdl_surface_format(dst->sdl_surface));
src_rect.x = 0;
src_rect.y = 0;
SDL_LowerBlit(tmp, &src_rect, dst->sdl_surface, &dst_rect);
SDL_FreeSurface(tmp);
SDL_FreeSurface(view);
sdl_destroy_surface(tmp);
sdl_destroy_surface(view);
return;
}
}
@@ -99,7 +100,7 @@ void gr_copy_bright(surface_t *dst, int dx, int dy, surface_t *src, int sx, int
void gr_fill(surface_t *dst, int dx, int dy, int dw, int dh, int r, int g, int b) {
SDL_Rect rect = {dx, dy, dw, dh};
uint32_t color = SDL_MapRGB(dst->sdl_surface->format, r, g, b);
uint32_t color = sdl_map_rgb(dst->sdl_surface, r, g, b);
SDL_FillRect(dst->sdl_surface, &rect, color);
}
@@ -227,12 +228,12 @@ void gr_copy_stretch_blend_alpha_map(surface_t *dst, int dx, int dy, int dw, int
// Fill the rectangle with the specified color (rgb) and blend rate (lv)
void gr_fill_alpha_color(surface_t *dst, int dx, int dy, int dw, int dh, int r, int g, int b, int lv) {
SDL_Surface *src = SDL_CreateRGBSurfaceWithFormat(0, dw, dh, 32, SDL_PIXELFORMAT_ARGB8888);
SDL_FillRect(src, NULL, SDL_MapRGBA(src->format, r, g, b, lv));
SDL_Surface *src = sdl_create_surface(dw, dh, 32, SDL_PIXELFORMAT_ARGB8888);
SDL_FillRect(src, NULL, sdl_map_rgba(src, r, g, b, lv));
SDL_SetSurfaceBlendMode(src, SDL_BLENDMODE_BLEND);
SDL_Rect dstrect = {dx, dy, dw, dh};
SDL_BlitSurface(src, NULL, dst->sdl_surface, &dstrect);
SDL_FreeSurface(src);
sdl_destroy_surface(src);
}
#include "graph2.c"
+6 -4
View File
@@ -3,7 +3,7 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <SDL.h>
#include "sdl_compat.h"
#include "portab.h"
#include "surface.h"
@@ -15,7 +15,8 @@ static surface_t *create(int width, int height, bool has_pixel, bool has_alpha)
s->height = height;
if (has_pixel) {
s->sdl_surface = SDL_CreateRGBSurfaceWithFormat(0, width, height, 32, SDL_PIXELFORMAT_RGB888);
s->sdl_surface = sdl_create_surface(
width, height, 32, SDL_PIXELFORMAT_RGB888);
}
if (has_alpha) {
@@ -47,7 +48,7 @@ surface_t *sf_create_pixel(int width, int height) {
*/
void sf_free(surface_t *s) {
if (s == NULL) return;
if (s->sdl_surface) SDL_FreeSurface(s->sdl_surface);
if (s->sdl_surface) sdl_destroy_surface(s->sdl_surface);
if (s->alpha) free(s->alpha);
free(s);
}
@@ -67,7 +68,8 @@ surface_t *sf_dup(surface_t *in) {
memcpy(sf, in, sizeof(surface_t));
if (in->sdl_surface) {
sf->sdl_surface = SDL_ConvertSurface(in->sdl_surface, in->sdl_surface->format, 0);
sf->sdl_surface = sdl_convert_surface(
in->sdl_surface, sdl_surface_format(in->sdl_surface));
}
if (in->alpha) {
+2 -2
View File
@@ -53,7 +53,7 @@ static void RandMTGet() {
var: 結果を返す変数
*/
int num = getCaliValue();
int *var = getCaliVariable();
vmvar_t *var = getCaliVariable();
if (num == 0 || num == 1) {
*var = num;
@@ -83,7 +83,7 @@ static void RandMTGetNumTable() {
var: 乱数を格納する変数
*/
int *var = getCaliVariable();
vmvar_t *var = getCaliVariable();
*var = (int)(genrand() * numtblmax) + 1;
+4 -4
View File
@@ -17,7 +17,7 @@ static bool valid;
static int action;
static void Init() {
int *p1 = getCaliVariable();
vmvar_t *p1 = getCaliVariable();
int p2 = getCaliValue(); /* ISys3x */
int p3 = getCaliValue();
int p4 = getCaliValue();
@@ -30,7 +30,7 @@ static void Init() {
}
static void Start() {
int *p1 = getCaliVariable();
vmvar_t *p1 = getCaliVariable();
int p2 = getCaliValue();
char *fname_utf8 = sjis2utf(svar_get(p2));
@@ -52,7 +52,7 @@ static void SetValid() {
}
static void GetValid() {
int *p1 = getCaliVariable();
vmvar_t *p1 = getCaliVariable();
*p1 = valid;
@@ -68,7 +68,7 @@ static void SetAction() {
}
static void GetAction() {
int *p1 = getCaliVariable();
vmvar_t *p1 = getCaliVariable();
*p1 = action;
+25 -26
View File
@@ -22,7 +22,7 @@ night_t nightprv;
static void Init(void) { /* 0 */
int *var = getCaliVariable();
vmvar_t *var = getCaliVariable();
int p1 = getCaliValue(); /* ISys3xCG */
int p2 = getCaliValue(); /* ISys3xDIB */
int p3 = getCaliValue(); /* ISys3xMsgString */
@@ -56,8 +56,7 @@ static void InitGame() { /* 1 */
nact->msgout = ntmsg_add;
nact->ags.eventcb = ntev_callback;
nact->callback = ntev_main;
nt_gr_init();
ntmsg_init();
nt_sstr_init();
@@ -214,7 +213,7 @@ static void SetSelMode(void) { /* 18 */
// キー入力待ち後、改ページ
static void AnalyzeMessage(void) { /* 19 */
int *var = getCaliVariable(); // 入力されたキー
vmvar_t *var = getCaliVariable(); // 入力されたキー
*var = ntmsg_ana();
@@ -298,7 +297,7 @@ static void ScreenCG(void) { /* 27 */
}
static void RunGameMain(void) { /* 28 */
int *p1 = getCaliVariable(); // result
vmvar_t *p1 = getCaliVariable(); // result
int p2 = getCaliValue(); // month
int p3 = getCaliValue(); // day
int p4 = getCaliValue(); // day of week
@@ -314,7 +313,7 @@ static void RunGameMain(void) { /* 28 */
}
static void CheckNewGame(void) { /* 29 */
int *p1 = getCaliVariable();
vmvar_t *p1 = getCaliVariable();
*p1 = 0;
@@ -348,9 +347,9 @@ static void SetDate(void) { /* 33 */
}
static void GetDate(void) { /* 34 */
int *p1 = getCaliVariable(); // month
int *p2 = getCaliVariable(); // day
int *p3 = getCaliVariable(); // day of weeek
vmvar_t *p1 = getCaliVariable(); // month
vmvar_t *p2 = getCaliVariable(); // day
vmvar_t *p3 = getCaliVariable(); // day of weeek
*p1 = night.Month;
*p2 = night.Day;
@@ -364,7 +363,7 @@ static void SelectGameLevel(void) { /* 35 */
}
static void RunEventDungeon(void) { /* 36 */
int *p1 = getCaliVariable();
vmvar_t *p1 = getCaliVariable();
int p2 = getCaliValue();
*p1 = 1;
@@ -479,7 +478,7 @@ static void RunSoundMode(void) { /* 48 */
}
static void RunMapEditor(void) { /* 49 */
int *p1 = getCaliVariable();
vmvar_t *p1 = getCaliVariable();
TRACE_UNIMPLEMENTED("NIGHTDLL.RunMapEditor %p:", p1);
}
@@ -496,21 +495,21 @@ static void VisualListAdd(void) { /* 51 */
}
static void GetLocalCountCG(void) { /* 52 */
int *p1 = getCaliVariable();
vmvar_t *p1 = getCaliVariable();
int p2 = getCaliValue();
TRACE_UNIMPLEMENTED("NIGHTDLL.GetLocalCountCG %p,%d:", p1, p2);
}
static void PlayMemory(void) { /* 53 */
int *p1 = getCaliVariable(); // 回想ページ
int *p2 = getCaliVariable(); // 回想RESULT
vmvar_t *p1 = getCaliVariable(); // 回想ページ
vmvar_t *p2 = getCaliVariable(); // 回想RESULT
TRACE_UNIMPLEMENTED("NIGHTDLL.PlayMemory %p,%p:", p1, p2);
}
static void GetEventFlagTotal(void) { /* 54 */
int *p1 = getCaliVariable();
vmvar_t *p1 = getCaliVariable();
int p2 = getCaliValue();
TRACE_UNIMPLEMENTED("NIGHTDLL.GetEventFlagTotal %p,%d:", p1, p2);
@@ -529,25 +528,25 @@ static void GetPlayerName(void) { /* 56 */
}
static void SaveGame(void) { /* 57 */
int *p1 = getCaliVariable();
vmvar_t *p1 = getCaliVariable();
TRACE_UNIMPLEMENTED("NIGHTDLL.SaveGame %p:", p1);
}
static void LoadGame(void) { /* 58 */
int *p1 = getCaliVariable();
vmvar_t *p1 = getCaliVariable();
TRACE_UNIMPLEMENTED("NIGHTDLL.LoadGame %p:", p1);
}
static void ExistSaveData(void) { /* 59 */
int *p1 = getCaliVariable();
vmvar_t *p1 = getCaliVariable();
TRACE_UNIMPLEMENTED("NIGHTDLL.ExistSaveData %p:", p1);
}
static void ExistStartData(void) { /* 60 */
int *p1 = getCaliVariable();
vmvar_t *p1 = getCaliVariable();
TRACE_UNIMPLEMENTED("NIGHTDLL.ExistStartData %p:", p1);
}
@@ -581,13 +580,13 @@ static void DebugScenario(void) { /* 66 */
}
static void GetDLLTime(void) { /* 67 */
int *p1 = getCaliVariable();
int *p2 = getCaliVariable();
int *p3 = getCaliVariable();
int *p4 = getCaliVariable();
int *p5 = getCaliVariable();
int *p6 = getCaliVariable();
int *p7 = getCaliVariable();
vmvar_t *p1 = getCaliVariable();
vmvar_t *p2 = getCaliVariable();
vmvar_t *p3 = getCaliVariable();
vmvar_t *p4 = getCaliVariable();
vmvar_t *p5 = getCaliVariable();
vmvar_t *p6 = getCaliVariable();
vmvar_t *p7 = getCaliVariable();
TRACE_UNIMPLEMENTED("NIGHTDLL.GetDLLTime %p,%p,%p,%p,%p,%p,%p:", p1, p2, p3, p4, p5, p6, p7);
}
-16
View File
@@ -53,11 +53,6 @@ static void cb_waitkey_selection(agsevent_t *e) {
}
void ntev_callback(agsevent_t *e) {
// menu open中は無視
if (nact->popupmenu_opened) {
return;
}
if (e->type == AGSEVENT_KEY_PRESS && e->code == KEY_CTRL) {
night.waitskiplv = 2;
night.waitkey = e->code;
@@ -90,16 +85,5 @@ void ntev_callback(agsevent_t *e) {
default:
return;
}
}
/*
system35のメインループからで呼ばれるコールバック
*/
void ntev_main() {
// デフォルトのコールバックのうち、ここで必要なものだけ処理。
if (nact->popupmenu_opened) {
menu_gtkmainiteration();
if (nact->is_quit) sys_exit(0);
}
}
-1
View File
@@ -2,7 +2,6 @@
#define __NT_EVENT_H__
extern void ntev_callback(agsevent_t *e);
extern void ntev_main();
#endif /* __NT_EVENT_H__ */
+2 -2
View File
@@ -1,5 +1,5 @@
#include <stdio.h>
#include <SDL.h>
#include "sdl_compat.h"
#include "portab.h"
#include "system.h"
@@ -194,5 +194,5 @@ void nt_gr_screencg(int no, int x, int y) {
SDL_SetSurfaceBlendMode(sf, SDL_BLENDMODE_ADD);
SDL_BlitSurface(sf, NULL, main_surface, &(SDL_Rect){x, y, sf->w, sf->h});
ags_updateArea(x, y, sf->w, sf->h);
SDL_FreeSurface(sf);
sdl_destroy_surface(sf);
}
+3 -3
View File
@@ -3,7 +3,7 @@
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <SDL.h>
#include "sdl_compat.h"
#include "portab.h"
#include "system.h"
@@ -142,7 +142,7 @@ void ntmsg_set_frame(int type) {
nt_sp_set_show(night.sp[SPNO_MSGFRAME_BG], true);
nt_sp_set_show(night.sp[SPNO_MSGFRAME_FG], true);
SDL_FillRect(sf, &msgframe_0, SDL_MapRGBA(sf->format, 16, 32, 64, 192));
SDL_FillRect(sf, &msgframe_0, sdl_map_rgba(sf, 16, 32, 64, 192));
ntmsg_clear(SPNO_MSGFRAME_FG);
nt_sp_update_all(true);
break;
@@ -151,7 +151,7 @@ void ntmsg_set_frame(int type) {
nt_sp_set_show(night.sp[SPNO_MSGFRAME_BG], false);
nt_sp_set_show(night.sp[SPNO_MSGFRAME_FG], true);
SDL_FillRect(sf, &msgframe_1, SDL_MapRGBA(sf->format, 32, 32, 32, 128));
SDL_FillRect(sf, &msgframe_1, sdl_map_rgba(sf, 32, 32, 32, 128));
ntmsg_clear(SPNO_MSGFRAME_FG);
nt_sp_update_all(true);
+10 -12
View File
@@ -55,19 +55,17 @@ static void ntmain(struct _scoadr inadr) {
while (!nact->is_quit && !is_yield_requested()) {
scheduler_on_command();
//SACT_DEBUG("%d:%x", sl_getPage(), sl_getIndex());
if (!nact->popupmenu_opened) {
exec_command();
if (sl_getPage() == inadr.page &&
sl_getIndex() == inadr.index) {
// ~E%05dからの戻り
if (nact->fnc_return_value == 0) {
return;
} else {
scono = nact->fnc_return_value;
}
curadr = scene2adr(scono);
sl_callFar2(curadr.page -1, curadr.index);
exec_command();
if (sl_getPage() == inadr.page &&
sl_getIndex() == inadr.index) {
// ~E%05dからの戻り
if (nact->fnc_return_value == 0) {
return;
} else {
scono = nact->fnc_return_value;
}
curadr = scene2adr(scono);
sl_callFar2(curadr.page -1, curadr.index);
}
nact->callback();
}
+4 -3
View File
@@ -1,7 +1,7 @@
#include "config.h"
#include <stdio.h>
#include <stdlib.h>
#include <SDL.h>
#include "sdl_compat.h"
#include "portab.h"
#include "sprite.h"
@@ -62,7 +62,8 @@ sprite_t *nt_sp_msg_new(int no, int x, int y, int width, int height) {
sp->height = height;
sp->u.msg.dspcur.x = 0;
sp->u.msg.dspcur.y = 0;
sp->u.msg.canvas = SDL_CreateRGBSurfaceWithFormat(0, width, height, 32, SDL_PIXELFORMAT_ARGB8888);
sp->u.msg.canvas = sdl_create_surface(
width, height, 32, SDL_PIXELFORMAT_ARGB8888);
sp->update = ntmsg_update;
return sp;
@@ -76,7 +77,7 @@ void nt_sp_free(sprite_t *sp) {
if (sp->cg3) scg_deref(sp->cg3);
if (sp->type == SPRITE_MSG) {
SDL_FreeSurface(sp->u.msg.canvas);
sdl_destroy_surface(sp->u.msg.canvas);
}
free(sp);
+1 -1
View File
@@ -3,7 +3,7 @@
#define __SPRITE_H__
#include "portab.h"
#include <SDL_rect.h>
#include "sdl_compat.h"
#include "sactcg.h"
#define DEFAULT_UPDATE nt_sp_draw
+2 -2
View File
@@ -24,7 +24,7 @@
#include "config.h"
#include <stdio.h>
#include <SDL.h>
#include "sdl_compat.h"
#include "portab.h"
#include "system.h"
@@ -55,7 +55,7 @@ void nt_sp_draw(sprite_t *sp, SDL_Rect *r) {
return;
}
if (SDL_ISPIXELFORMAT_ALPHA(cg->sf->format->format) || sp->blendrate < 255) {
if (SDL_ISPIXELFORMAT_ALPHA(sdl_surface_format(cg->sf)) || sp->blendrate < 255) {
SDL_SetSurfaceBlendMode(cg->sf, SDL_BLENDMODE_BLEND);
SDL_SetSurfaceAlphaMod(cg->sf, sp->blendrate);
} else {
+2 -2
View File
@@ -25,7 +25,7 @@
#include <stdio.h>
#include <stdlib.h>
#include <SDL.h>
#include "sdl_compat.h"
#include "portab.h"
#include "system.h"
@@ -204,5 +204,5 @@ void nt_sp_clear_updatelist(void) {
// デフォルトの壁紙update
void nt_sp_draw_wall(sprite_t *sp, SDL_Rect *area) {
SDL_FillRect(main_surface, area, SDL_MapRGB(main_surface->format, 0, 0, 0));
SDL_FillRect(main_surface, area, sdl_map_rgb(main_surface, 0, 0, 0));
}
+4 -3
View File
@@ -60,10 +60,11 @@ static void ndd_run(int demonum) {
cgdata *cg = jpeg_extract(alk->entries[i].data, alk->entries[i].size);
if (cg) {
SDL_Surface *sf = SDL_CreateRGBSurfaceWithFormatFrom(cg->pic, cg->width, cg->height, 24, cg->width * 3, SDL_PIXELFORMAT_RGB24);
SDL_Surface *sf = sdl_create_surface_from(cg->pic, cg->width,
cg->height, 24, cg->width * 3, SDL_PIXELFORMAT_RGB24);
SDL_BlitSurface(sf, NULL, main_surface, NULL);
ags_updateFull();
SDL_FreeSurface(sf);
sdl_destroy_surface(sf);
cgdata_free(cg);
} else {
WARNING("Cannot decode CG %d in %s", i, alk_path);
@@ -90,7 +91,7 @@ static void Init() {
int p1 = getCaliValue(); /* ISys3x */
int p2 = getCaliValue(); /* IWinMsg */
int p3 = getCaliValue(); /* ITimer */
int *var = getCaliVariable();
vmvar_t *var = getCaliVariable();
*var = 1;
+2 -2
View File
@@ -51,7 +51,7 @@ static void Get() {
var: 結果を返す変数
*/
int num = getCaliValue();
int *var = getCaliVariable();
vmvar_t *var = getCaliVariable();
if (num == 0 || num == 1) {
*var = num;
@@ -65,7 +65,7 @@ static void Get() {
static void GetNoOverlap() {
int min = getCaliValue();
int n = getCaliValue();
int *var = getCaliVariable();
vmvar_t *var = getCaliVariable();
*var = (int)(genrand() * n) + min;
+9 -9
View File
@@ -42,7 +42,7 @@ static void CreateChannel(void) {
static void Prepare(void) {
int ch = getCaliValue();
int no = getCaliValue();
int *result = getCaliVariable();
vmvar_t *result = getCaliVariable();
if (ch == 0) {
current_no = no;
@@ -97,7 +97,7 @@ static void PlayPosSample(void) {
static void IsPlay(void) {
int ch = getCaliValue();
int *result = getCaliVariable();
vmvar_t *result = getCaliVariable();
if (ch == 0) {
*result = current_no && musbgm_isplaying(current_no);
@@ -127,10 +127,10 @@ static void Restart(void) {
static void GetPlayPos(void) {
int ch = getCaliValue();
int *hour = getCaliVariable();
int *min = getCaliVariable();
int *sec = getCaliVariable();
int *msec = getCaliVariable();
vmvar_t *hour = getCaliVariable();
vmvar_t *min = getCaliVariable();
vmvar_t *sec = getCaliVariable();
vmvar_t *msec = getCaliVariable();
if (ch == 0) {
int time = musbgm_getpos(current_no);
*hour = time / 360000;
@@ -149,8 +149,8 @@ static void GetPlayPos(void) {
static void GetPlayPosSample(void) {
int p1 = getCaliValue();
int *var1 = getCaliVariable();
int *var2 = getCaliVariable();
vmvar_t *var1 = getCaliVariable();
vmvar_t *var2 = getCaliVariable();
TRACE_UNIMPLEMENTED("S3xMusic.GetPlayPosSample %d, %p, %p:", p1, var1, var2);
}
@@ -192,7 +192,7 @@ static void FadeVolume(void) {
static void IsFade(void) {
int ch = getCaliValue();
int *result = getCaliVariable();
vmvar_t *result = getCaliVariable();
*result = 0;
TRACE_UNIMPLEMENTED("S3xMusic.IsFade %d => %d:", ch, *result);
}
+73 -63
View File
@@ -25,7 +25,7 @@
#include <stdio.h>
#include <string.h>
#include <SDL.h>
#include "sdl_compat.h"
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#endif
@@ -33,6 +33,7 @@
#include "portab.h"
#include "system.h"
#include "ald_manager.h"
#include "audio_meta.h"
#include "input.h"
#include "msgskip.h"
#include "xsystem35.h"
@@ -146,7 +147,8 @@ static void Init() {
}
// create depth map
sact.dmap = SDL_CreateRGBSurface(0, main_surface->w, main_surface->h, 16, 0, 0, 0, 0);
sact.dmap = sdl_create_surface_with_masks(
main_surface->w, main_surface->h, 16, 0, 0, 0, 0);
// その他 System35 のデフォルト動作の変更
ags_setAntialiasedStringMode(true);
@@ -286,8 +288,11 @@ static void DrawEffect() {
if (sact.version >= 110) {
wEffectkey = getCaliValue();
}
sp_eupdate(wType, wEffectTime, wEffectkey);
if (sact.waitskiplv > 1 || msgskip_isSkipping())
sp_update_all(true);
else
sp_eupdate(wType, wEffectTime, wEffectkey);
TRACE("SACT.DrawEffect %d,%d,%d:", wType, wEffectTime, wEffectkey);
}
@@ -304,7 +309,10 @@ static void DrawEffectAlphaMap() {
int wEffectTime = getCaliValue();
int wEffectKey = getCaliValue();
sp_eupdate_amap(nIndexAlphaMap, wEffectTime, wEffectKey);
if (sact.waitskiplv > 1 || msgskip_isSkipping())
sp_update_all(true);
else
sp_eupdate_amap(nIndexAlphaMap, wEffectTime, wEffectKey);
TRACE("SACT.DrawEffectAlphaMap %d,%d,%d:", nIndexAlphaMap, wEffectTime, wEffectKey);
}
@@ -622,9 +630,10 @@ static void QuakeSprite() {
if (sact.version >= 110) {
nfKeyEnable = getCaliValue();
}
sp_quake_sprite(wType, wAmplitudeX, wAmplitudeY, wCount, nfKeyEnable);
if (!msgskip_isSkipping())
sp_quake_sprite(wType, wAmplitudeX, wAmplitudeY, wCount, nfKeyEnable);
TRACE("SACT.QuakeSprite %d,%d,%d,%d:", wType, wAmplitudeX, wAmplitudeY, wCount);
}
@@ -636,7 +645,7 @@ static void QuakeSprite() {
*/
static void QuerySpriteIsExist() {
int wNum = getCaliValue();
int *var = getCaliVariable();
vmvar_t *var = getCaliVariable();
*var = sp_exists(wNum) ? 1 : 0;
@@ -654,10 +663,10 @@ static void QuerySpriteIsExist() {
*/
static void QuerySpriteInfo() {
int wNum = getCaliValue();
int *vType = getCaliVariable();
int *vCG1 = getCaliVariable();
int *vCG2 = getCaliVariable();
int *vCG3 = getCaliVariable();
vmvar_t *vType = getCaliVariable();
vmvar_t *vCG1 = getCaliVariable();
vmvar_t *vCG2 = getCaliVariable();
vmvar_t *vCG3 = getCaliVariable();
sp_query_info(wNum, vType, vCG1, vCG2, vCG3);
@@ -672,7 +681,7 @@ static void QuerySpriteInfo() {
*/
static void QuerySpriteShow() {
int wNum = getCaliValue();
int *vShow = getCaliVariable();
vmvar_t *vShow = getCaliVariable();
sp_query_show(wNum, vShow);
@@ -688,8 +697,8 @@ static void QuerySpriteShow() {
*/
static void QuerySpritePos() {
int wNum = getCaliValue();
int *vX = getCaliVariable();
int *vY = getCaliVariable();
vmvar_t *vX = getCaliVariable();
vmvar_t *vY = getCaliVariable();
sp_query_pos(wNum, vX, vY);
@@ -705,8 +714,8 @@ static void QuerySpritePos() {
*/
static void QuerySpriteSize() {
int wNum = getCaliValue();
int *vWidth = getCaliVariable();
int *vHeight = getCaliVariable();
vmvar_t *vWidth = getCaliVariable();
vmvar_t *vHeight = getCaliVariable();
sp_query_size(wNum, vWidth, vHeight);
@@ -722,8 +731,8 @@ static void QuerySpriteSize() {
*/
static void QueryTextPos() {
int wNum = getCaliValue();
int *vX = getCaliVariable();
int *vY = getCaliVariable();
vmvar_t *vX = getCaliVariable();
vmvar_t *vY = getCaliVariable();
sp_query_textpos(wNum, vX, vY);
@@ -762,7 +771,7 @@ static void CG_Reset() {
*/
static void CG_QueryType() {
int wNumCG = getCaliValue();
int *vType = getCaliVariable();
vmvar_t *vType = getCaliVariable();
*vType = scg_querytype(wNumCG);
@@ -778,8 +787,8 @@ static void CG_QueryType() {
*/
static void CG_QuerySize() {
int wNumCG = getCaliValue();
int *vWidth = getCaliVariable();
int *vHeight = getCaliVariable();
vmvar_t *vWidth = getCaliVariable();
vmvar_t *vHeight = getCaliVariable();
scg_querysize(wNumCG, vWidth, vHeight);
@@ -794,7 +803,7 @@ static void CG_QuerySize() {
*/
static void CG_QueryBpp() {
int wNumCG = getCaliValue();
int *vBpp = getCaliVariable();
vmvar_t *vBpp = getCaliVariable();
*vBpp = scg_querybpp(wNumCG);
@@ -809,7 +818,7 @@ static void CG_QueryBpp() {
*/
static void CG_ExistAlphaMap() {
int wNumCG = getCaliValue();
int *vMask = getCaliVariable();
vmvar_t *vMask = getCaliVariable();
*vMask = scg_existalphamap(wNumCG) ? 1 : 0;
@@ -1021,7 +1030,7 @@ static void CG_PartCopy() {
* @param vKey: 入力されたキー
*/
static void WaitKeySimple() {
int *vKey = getCaliVariable();
vmvar_t *vKey = getCaliVariable();
TRACE("SACT.WaitKeySimple %d:", vKey);
@@ -1071,10 +1080,10 @@ static void WaitKeyMessage() {
* @param vRsv2: 予約
*/
static void WaitKeySprite() {
int *vOK = getCaliVariable();
int *vRND = getCaliVariable();
int *vRsv1 = getCaliVariable();
int *vRsv2 = getCaliVariable();
vmvar_t *vOK = getCaliVariable();
vmvar_t *vRND = getCaliVariable();
vmvar_t *vRsv1 = getCaliVariable();
vmvar_t *vRsv2 = getCaliVariable();
TRACE("SACT.WaitKeySprite %p,%p,%p,%p:", vOK, vRND, vRsv1, vRsv2);
@@ -1092,7 +1101,7 @@ static void WaitKeySprite() {
static void PeekKey() {
static int prevKeyCode = NUM_KEYCODES;
int nKeyCode = getCaliValue();
int *vResult = getCaliVariable();
vmvar_t *vResult = getCaliVariable();
// This function is called successively with different nKeyCodes.
// Only the first call hits the scheduler.
@@ -1121,8 +1130,8 @@ static void WaitMsgSkipKeyUp() {
* @param wTime: タイムアウト時間 (1/100sec)
*/
static void WaitKeySimpleTimeOut() {
int *vRND = getCaliVariable();
int *vD03 = getCaliVariable();
vmvar_t *vRND = getCaliVariable();
vmvar_t *vD03 = getCaliVariable();
int wTime = getCaliValue();
sact.waittype = KEYWAIT_SIMPLE;
@@ -1153,11 +1162,11 @@ static void WaitKeySimpleTimeOut() {
* @param wTime: タイムアウト時間 (1/100sec)
*/
static void WaitKeySpriteTimeOut() {
int *vOK = getCaliVariable();
int *vRND = getCaliVariable();
int *vD01 = getCaliVariable();
int *vD02 = getCaliVariable();
int *vD03 = getCaliVariable();
vmvar_t *vOK = getCaliVariable();
vmvar_t *vRND = getCaliVariable();
vmvar_t *vD01 = getCaliVariable();
vmvar_t *vD02 = getCaliVariable();
vmvar_t *vD03 = getCaliVariable();
int wTime = getCaliValue();
sp_keywait(vOK, vRND, vD01, vD02, vD03, wTime);
@@ -1171,7 +1180,7 @@ static void WaitKeySpriteTimeOut() {
* @param vSkip:
*/
static void QueryMessageSkip() {
int *vSkip = getCaliVariable();
vmvar_t *vSkip = getCaliVariable();
*vSkip = msgskip_isSkipping() ? 1 : 0;
@@ -1219,7 +1228,7 @@ static void MessageOutput() {
int wMessageSpeed = getCaliValue();
int wMessageLineSpace = getCaliValue();
int wMessageAlign = 0;
int *vMessageLength = NULL;
vmvar_t *vMessageLength = NULL;
if (sact.version >= 110) {
wMessageAlign = getCaliValue();
@@ -1266,7 +1275,7 @@ static void MessageOutputEx() {
int wRubySize = getCaliValue();
int wRubyFont = getCaliValue();
int wRubyLineSpace = getCaliValue();
int *vLength = NULL;
vmvar_t *vLength = NULL;
if (sact.version >= 120) {
vLength = getCaliVariable();
@@ -1311,7 +1320,7 @@ static void MessageClear() {
* @param wResult: 結果を返す変数
*/
static void MessageIsEmpty() {
int *wResult = getCaliVariable();
vmvar_t *wResult = getCaliVariable();
*wResult = smsg_is_empty() ? 1 : 0;
@@ -1325,7 +1334,7 @@ static void MessageIsEmpty() {
* @param nTopStringNum: バッファを取得する文字列変数の最初
*/
static void MessagePeek() {
int *vCount = getCaliVariable();
vmvar_t *vCount = getCaliVariable();
int nTopStringNum = getCaliValue();
*vCount = smsg_peek(nTopStringNum);
@@ -1388,7 +1397,7 @@ static void MenuAdd() {
* @param nAlign: 行そろえ (0:左, 1:中央, 2: 右) (1.1~)
*/
static void MenuOpen() {
int *wMenuResult = getCaliVariable();
vmvar_t *wMenuResult = getCaliVariable();
int wNum = getCaliValue();
int wChoiceSize = getCaliValue();
int wMenuOutSpc = getCaliValue();
@@ -1464,7 +1473,7 @@ static void Numeral_SetCG() {
static void Numeral_GetCG() {
int nNum = getCaliValue();
int nIndex = getCaliValue();
int *vCG = getCaliVariable();
vmvar_t *vCG = getCaliVariable();
sp_num_getcg(nNum, nIndex, vCG);
@@ -1497,8 +1506,8 @@ static void Numeral_SetPos() {
*/
static void Numeral_GetPos() {
int nNum = getCaliValue();
int *vX = getCaliVariable();
int *vY = getCaliVariable();
vmvar_t *vX = getCaliVariable();
vmvar_t *vY = getCaliVariable();
sp_num_getpos(nNum, vX, vY);
@@ -1528,7 +1537,7 @@ static void Numeral_SetSpan() {
*/
static void Numeral_GetSpan() {
int nNum = getCaliValue();
int *vSpan = getCaliVariable();
vmvar_t *vSpan = getCaliVariable();
sp_num_getspan(nNum, vSpan);
@@ -1596,7 +1605,7 @@ static void TimerSet() {
*/
static void TimerGet() {
int wTimerID = getCaliValue();
int *vRND = getCaliVariable();
vmvar_t *vRND = getCaliVariable();
*vRND = stimer_get(wTimerID);
@@ -1615,7 +1624,7 @@ static void TimerWait() {
int msec = (wCount - stimer_get(wTimerID)) * 10;
if (msec > 0)
sys_keywait(msec, KEYWAIT_CTRL_CANCELABLE);
sys_keywait(msec, KEYWAIT_CTRL_CANCELABLE | KEYWAIT_SKIPPABLE);
TRACE("SACT.TimerWait %d,%d:", wTimerID, wCount);
}
@@ -1628,7 +1637,7 @@ static void TimerWait() {
static void Wait() {
int wCount = getCaliValue();
sys_keywait(wCount * 10, KEYWAIT_CTRL_CANCELABLE);
sys_keywait(wCount * 10, KEYWAIT_CTRL_CANCELABLE | KEYWAIT_SKIPPABLE);
TRACE("SACT.Wait %d:", wCount);
}
@@ -1696,7 +1705,7 @@ static void SoundWait() {
*/
static void SoundWaitKey() {
int wNum = getCaliValue();
int *vKey = getCaliVariable();
vmvar_t *vKey = getCaliVariable();
ssnd_waitkey(wNum, vKey);
@@ -1797,7 +1806,7 @@ static void SpriteSoundOB() {
*/
static void MusicCheck() {
int wNum = getCaliValue();
int *vRND = getCaliVariable();
vmvar_t *vRND = getCaliVariable();
*vRND = ald_exists(DRIFILE_BGM, wNum - 1) ? 1 : 0;
@@ -1812,7 +1821,7 @@ static void MusicCheck() {
*/
static void MusicGetLength() {
int wNum = getCaliValue();
int *vRND = getCaliVariable();
vmvar_t *vRND = getCaliVariable();
*vRND = musbgm_getlen(wNum);
@@ -1827,7 +1836,7 @@ static void MusicGetLength() {
*/
static void MusicGetPos() {
int wNum = getCaliValue();
int *vRND = getCaliVariable();
vmvar_t *vRND = getCaliVariable();
*vRND = musbgm_getpos(wNum);
@@ -1845,8 +1854,9 @@ static void MusicPlay() {
int wNum = getCaliValue();
int wFadeTime = getCaliValue();
int wVolume = getCaliValue();
bgi_t *bgi = bgi_find(wNum);
musbgm_play(wNum, wFadeTime, wVolume, 0);
musbgm_play(wNum, wFadeTime, wVolume, bgi ? bgi->loopno : 0);
TRACE("SACT.MusicPlay %d,%d,%d:", wNum, wFadeTime, wVolume);
}
@@ -1937,7 +1947,7 @@ static void MusicWaitPos() {
*/
static void SoundGetLinkNum() {
int wNum = getCaliValue();
int *vRND = getCaliVariable();
vmvar_t *vRND = getCaliVariable();
*vRND = ssnd_getlinknum(wNum);
@@ -1957,7 +1967,7 @@ static void SoundGetLinkNum() {
* pos = ((pos2-pos1) / (val2-val1)) * (val-val1) + pos1
*/
static void ChartPos() {
int *pos = getCaliVariable();
vmvar_t *pos = getCaliVariable();
int pos1 = getCaliValue();
int pos2 = getCaliValue();
int val1 = getCaliValue();
@@ -2002,7 +2012,7 @@ static void Maze_Create() {
* SACT.Maze_Get (1.0~)
*/
static void Maze_Get() {
int *p1 = getCaliVariable();
vmvar_t *p1 = getCaliVariable();
int p2 = getCaliValue();
int p3 = getCaliValue();
@@ -2013,7 +2023,7 @@ static void Maze_Get() {
* SACT.EncryptWORD (1.0~)
*/
static void EncryptWORD() {
int *array = getCaliVariable();
vmvar_t *array = getCaliVariable();
int num = getCaliValue();
int key = getCaliValue();
@@ -2026,7 +2036,7 @@ static void EncryptWORD() {
* SACT.DecryptWORD (1.0~)
*/
static void DecryptWORD() {
int *array = getCaliVariable();
vmvar_t *array = getCaliVariable();
int num = getCaliValue();
int key = getCaliValue();
@@ -2092,7 +2102,7 @@ static void XMenuRegister() {
*/
static void XMenuGetNum() {
int nRegiNum = getCaliValue();
int *vMenuID = getCaliVariable();
vmvar_t *vMenuID = getCaliVariable();
*vMenuID = spxm_getnum(nRegiNum);
@@ -2145,7 +2155,7 @@ static void SACT_reset(void) {
sp_reset();
scg_freeall();
SDL_FreeSurface(sact.dmap);
sdl_destroy_surface(sact.dmap);
memset(&sact, 0, sizeof(sact));
}
+1 -1
View File
@@ -25,7 +25,7 @@
#define __SACT_H__
#include "config.h"
#include <SDL_rect.h>
#include "sdl_compat.h"
#include "portab.h"
#include "list.h"
#include "ags.h"
+4 -3
View File
@@ -25,7 +25,7 @@
#include <stdio.h>
#include <stdlib.h>
#include <SDL.h>
#include "sdl_compat.h"
#include "portab.h"
#include "system.h"
@@ -90,13 +90,14 @@ void sp_eupdate_amap(int index, int time, int cancel) {
sp_update_all(true);
return;
}
SDL_Surface *mask_sf = SDL_CreateRGBSurfaceFrom(mask->pic, mask->width, mask->height, 8, mask->width, 0, 0, 0, 0);
SDL_Surface *mask_sf = sdl_create_surface_from(mask->pic, mask->width,
mask->height, 8, mask->width, SDL_PIXELFORMAT_INDEX8);
sp_update_all(false); // old = gfx_texture, new = main_surface
struct effect *eff = effect_sactamask_init(mask_sf);
ags_runEffect(time * 10, cancel, (ags_EffectStepFunc)effect_step, eff);
effect_finish(eff);
ags_updateFull();
SDL_FreeSurface(mask_sf);
sdl_destroy_surface(mask_sf);
cgdata_free(mask);
}
+1 -1
View File
@@ -25,7 +25,7 @@
#include "portab.h"
// グラフ用チャート作成
void schart_pos(int *pos, int pos1, int pos2, int val1, int val2, int val) {
void schart_pos(vmvar_t *pos, int pos1, int pos2, int val1, int val2, int val) {
if (val1 == val2) {
*pos = 0;
} else {
+3 -1
View File
@@ -24,6 +24,8 @@
#ifndef __SACTCHART_H__
#define __SACTCHART_H__
void schart_pos(int *pos, int pos1, int pos2, int val1, int val2, int val);
#include "portab.h"
void schart_pos(vmvar_t *pos, int pos1, int pos2, int val1, int val2, int val);
#endif
+1 -1
View File
@@ -31,7 +31,7 @@
適当でいいとおもう
*/
void scryp_encrypt_word(int *array, int num, int key) {
void scryp_encrypt_word(vmvar_t *array, int num, int key) {
WARNING("NOT IMPLEMENTED");
}
+3 -1
View File
@@ -24,7 +24,9 @@
#ifndef __SACTCRYPT_H__
#define __SACTCRYPT_H__
void scryp_encrypt_word(int *array, int num, int key);
#include "portab.h"
void scryp_encrypt_word(vmvar_t *array, int num, int key);
void scryp_decrypt_word(int *array, int num, int key);
void scryp_encrypt_str(int strno, int key);
void scryp_decrypt_str(int strno, int key);
+11 -6
View File
@@ -26,7 +26,7 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <SDL.h>
#include "sdl_compat.h"
#include "portab.h"
#include "system.h"
@@ -73,8 +73,9 @@ static void draw_log() {
load_mincho_font();
#endif
SDL_Surface *hline = SDL_CreateRGBSurfaceWithFormat(0, main_surface->w, 3, 32, SDL_PIXELFORMAT_RGB888);
SDL_FillRect(hline, NULL, SDL_MapRGB(hline->format, 255, 255, 255));
SDL_Surface *hline = sdl_create_surface(
main_surface->w, 3, 32, SDL_PIXELFORMAT_RGB888);
SDL_FillRect(hline, NULL, sdl_map_rgb(hline, 255, 255, 255));
SDL_SetSurfaceBlendMode(hline, SDL_BLENDMODE_BLEND);
SDL_SetSurfaceAlphaMod(hline, 128);
@@ -97,7 +98,11 @@ static void draw_log() {
if (0 == strcmp(str, "\n")) {
SDL_BlitSurface(hline, NULL, main_surface, &(SDL_Rect){0, y + FONTSIZE/2, main_surface->w, 3});
} else {
FontSpec font_spec = { .type = cur < 6 ? FONT_MINCHO : FONT_GOTHIC, .size = FONTSIZE };
FontSpec font_spec = {
.type = cur < 6 ? FONT_MINCHO : FONT_GOTHIC,
.weight = cur < 6 ? FONT_WEIGHT_NORMAL : FONT_WEIGHT_BOLD,
.size = FONTSIZE
};
gfx_drawString(0, y, str, 255, font_spec);
}
y += FONTSIZE;
@@ -118,7 +123,7 @@ bool sblog_start(void) {
for (int i = 0; i < LOGMSG_LINES; i++)
sact.log = list_append(sact.log, logmsg[i]);
back = SDL_ConvertSurface(main_surface, main_surface->format, 0);
back = sdl_convert_surface(main_surface, sdl_surface_format(main_surface));
curline = 6;
draw_log();
return true;
@@ -131,7 +136,7 @@ void sblog_end(void) {
SDL_BlitSurface(back, NULL, main_surface, NULL);
ags_updateFull();
SDL_FreeSurface(back);
sdl_destroy_surface(back);
// Remove instruction text
for (i = 0; i < LOGMSG_LINES; i++) {
+3 -2
View File
@@ -30,6 +30,7 @@
#include "system.h"
#include "nact.h"
#include "input.h"
#include "msgskip.h"
#include "sactsound.h"
#include "music.h"
#include "sact.h"
@@ -112,7 +113,7 @@ void ssnd_wait(int no) {
}
// 指定の効果音が終了するか、キーが押されるまで待つ
void ssnd_waitkey(int no, int *res) {
void ssnd_waitkey(int no, vmvar_t *res) {
int slot = slt_find(no);
if (slot == -1) {
@@ -120,7 +121,7 @@ void ssnd_waitkey(int no, int *res) {
return;
}
if (sact.waitskiplv > 1) {
if (sact.waitskiplv > 1 || msgskip_isSkipping()) {
*res = SYS35KEY_RET;
return;
}
+3 -1
View File
@@ -24,11 +24,13 @@
#ifndef __SACTSOUND_H__
#define __SACTSOUND_H__
#include "portab.h"
void ssnd_init(void);
void ssnd_play(int no);
void ssnd_stop(int no, int fadetime);
void ssnd_wait(int no);
void ssnd_waitkey(int no, int *res);
void ssnd_waitkey(int no, vmvar_t *res);
void ssnd_prepare(int no);
void ssnd_prepareLRrev(int no);
void ssnd_playLRrev(int no);
+1 -1
View File
@@ -28,7 +28,7 @@
#include "sacttimer.h"
#include "system.h"
#define MAX_TIMER 10
#define MAX_TIMER 128
#define TICKS_PER_CENTISECOND 10
uint32_t ticks_base[MAX_TIMER];
+14 -12
View File
@@ -26,7 +26,7 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <SDL.h>
#include "sdl_compat.h"
#include "portab.h"
#include "system.h"
@@ -65,7 +65,8 @@ static int compare_spriteno_smallfirst(const void *a, const void *b) {
// デフォルトの壁紙update
static void sp_draw_wall(sprite_t *sp) {
SDL_FillRect(main_surface, &sact.updaterect, SDL_MapRGB(main_surface->format, 0, 0, 0));
SDL_FillRect(main_surface, &sact.updaterect,
sdl_map_rgb(main_surface, 0, 0, 0));
}
/**
@@ -207,7 +208,8 @@ void sp_new_msg(int no, int x, int y, int width, int height) {
sp->u.msg.buf = NULL;
// canvas for drawing text
sp->u.msg.canvas = SDL_CreateRGBSurfaceWithFormat(0, width, height, 32, SDL_PIXELFORMAT_ARGB8888);
sp->u.msg.canvas = sdl_create_surface(
width, height, 32, SDL_PIXELFORMAT_ARGB8888);
// スプライト再描画用コールバック
sp->update = smsg_update;
@@ -279,7 +281,7 @@ void sp_free(int no) {
if (sp->type == SPRITE_MSG) {
slist_free(sp->u.msg.buf);
SDL_FreeSurface(sp->u.msg.canvas);
sdl_destroy_surface(sp->u.msg.canvas);
}
sact.updatelist = slist_remove(sact.updatelist, sp);
@@ -455,7 +457,7 @@ bool sp_exists(int wNum) {
}
// スプライトのタイプと何番のCGがセットされているかの取得
bool sp_query_info(int wNum, int *vtype, int *vcg1, int *vcg2, int *vcg3) {
bool sp_query_info(int wNum, vmvar_t *vtype, vmvar_t *vcg1, vmvar_t *vcg2, vmvar_t *vcg3) {
sprite_t *sp;
if (wNum >= SPRITEMAX) goto errexit;
@@ -479,7 +481,7 @@ bool sp_query_info(int wNum, int *vtype, int *vcg1, int *vcg2, int *vcg3) {
}
// スプライトの表示状態の取得
bool sp_query_show(int wNum, int *vShow) {
bool sp_query_show(int wNum, vmvar_t *vShow) {
if (wNum >= SPRITEMAX) goto errexit;
if (sact.sp[wNum]->type == SPRITE_NONE) goto errexit;
@@ -492,7 +494,7 @@ bool sp_query_show(int wNum, int *vShow) {
}
// スプライトの表示位置の取得
bool sp_query_pos(int wNum, int *vx, int *vy) {
bool sp_query_pos(int wNum, vmvar_t *vx, vmvar_t *vy) {
if (wNum >= SPRITEMAX) goto errexit;
if (sact.sp[wNum]->type == SPRITE_NONE) goto errexit;
@@ -507,7 +509,7 @@ bool sp_query_pos(int wNum, int *vx, int *vy) {
}
// スプライトの大きさの取得
bool sp_query_size(int wNum, int *vw, int *vh) {
bool sp_query_size(int wNum, vmvar_t *vw, vmvar_t *vh) {
sprite_t *sp;
if (wNum >= SPRITEMAX) goto errexit;
@@ -528,7 +530,7 @@ bool sp_query_size(int wNum, int *vw, int *vh) {
}
// テキストスプライトの現在の文字表示位置の取得
bool sp_query_textpos(int wNum, int *vx, int *vy) {
bool sp_query_textpos(int wNum, vmvar_t *vx, vmvar_t *vy) {
if (wNum >= SPRITEMAX) goto errexit;
sprite_t *sp = sact.sp[wNum];
if (sp->type != SPRITE_MSG) goto errexit;
@@ -551,7 +553,7 @@ void sp_num_setcg(int nNum, int nIndex, int nCG) {
}
// NumeralXXXのCGの取得
void sp_num_getcg(int nNum, int nIndex, int *vCG) {
void sp_num_getcg(int nNum, int nIndex, vmvar_t *vCG) {
SP_ASSERT_NO(nNum);
*vCG = sact.sp[nNum]->numeral.cg[nIndex];
@@ -566,7 +568,7 @@ void sp_num_setpos(int nNum, int nX, int nY) {
}
// NumeralXXXの位置の取得
void sp_num_getpos(int nNum, int *vX, int *vY) {
void sp_num_getpos(int nNum, vmvar_t *vX, vmvar_t *vY) {
SP_ASSERT_NO(nNum);
*vX = sact.sp[nNum]->numeral.pos.x;
@@ -581,7 +583,7 @@ void sp_num_setspan(int nNum, int nSpan) {
}
// NumeralXXXのスパンの取得
void sp_num_getspan(int nNum, int *vSpan) {
void sp_num_getspan(int nNum, vmvar_t *vSpan) {
SP_ASSERT_NO(nNum);
*vSpan = sact.sp[nNum]->numeral.span;
+11 -10
View File
@@ -24,6 +24,7 @@
#ifndef __SPRITE_H__
#define __SPRITE_H__
#include "portab.h"
#include "sact.h"
#define DEFAULT_UPDATE sp_draw
@@ -51,17 +52,17 @@ void sp_set_animeinterval(int wNum, int wTime);
bool sp_is_insprite(sprite_t *sp, int x, int y);
void sp_set_blendrate(int wNum, int wCount, int rate);
bool sp_exists(int wNum);
bool sp_query_info(int wNum, int *vtype, int *vcg1, int *vcg2, int *vcg3);
bool sp_query_show(int wNum, int *vShow);
bool sp_query_pos(int wNum, int *vx, int *vy);
bool sp_query_size(int wNum, int *vw, int *vh);
bool sp_query_textpos(int wNum, int *vx, int *vy);
bool sp_query_info(int wNum, vmvar_t *vtype, vmvar_t *vcg1, vmvar_t *vcg2, vmvar_t *vcg3);
bool sp_query_show(int wNum, vmvar_t *vShow);
bool sp_query_pos(int wNum, vmvar_t *vx, vmvar_t *vy);
bool sp_query_size(int wNum, vmvar_t *vw, vmvar_t *vh);
bool sp_query_textpos(int wNum, vmvar_t *vx, vmvar_t *vy);
void sp_num_setcg(int nNum, int nIndex, int nCG);
void sp_num_getcg(int nNum, int nIndex, int *vCG);
void sp_num_getcg(int nNum, int nIndex, vmvar_t *vCG);
void sp_num_setpos(int nNum, int nX, int nY);
void sp_num_getpos(int nNum, int *vX, int *vY);
void sp_num_getpos(int nNum, vmvar_t *vX, vmvar_t *vY);
void sp_num_setspan(int nNum, int nSpan);
void sp_num_getspan(int nNUm, int *vSpan);
void sp_num_getspan(int nNUm, vmvar_t *vSpan);
void sp_exp_clear(void);
void sp_exp_add(int nNumSP1, int nNumSP2);
void sp_exp_del(int wNum);
@@ -85,7 +86,7 @@ void sp_draw_dmap(void* data, void* userdata);
// in sprite_msg.c
void smsg_add(const char *msg);
void smsg_newline(int wNum, int size);
void smsg_out(int wNum, int wSize, int wColorR, int wColorG, int wColorB, int wFont, int wSpeed, int wLineSpace, int wAlign, int wRSize, int wRFont, int wRLineSpace, int *wLength);
void smsg_out(int wNum, int wSize, int wColorR, int wColorG, int wColorB, int wFont, int wSpeed, int wLineSpace, int wAlign, int wRSize, int wRFont, int wRLineSpace, vmvar_t *wLength);
void smsg_clear(int wNum);
bool smsg_is_empty();
int smsg_peek(int nTopStringNum);
@@ -133,7 +134,7 @@ void sp_eupdate(int type, int time, int key);
void sp_quake_sprite(int wType, int wAmplitudeX, int wAmplitude, int wCount, int cancel);
// in sprite_keywait.c
void sp_keywait(int *vOK, int *vRND, int *vRsv1, int *vRsv2, int *vRsv3, int timeout);
void sp_keywait(vmvar_t *vOK, vmvar_t *vRND, vmvar_t *vRsv1, vmvar_t *vRsv2, vmvar_t *vRsv3, int timeout);
// in screen_quake.c
void sp_quake_screen(int type, int p1, int p2, int time, int cancel);
+4 -4
View File
@@ -26,7 +26,7 @@
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include <SDL.h>
#include "sdl_compat.h"
#include "portab.h"
#include "system.h"
@@ -38,7 +38,7 @@
static void fill_dmap_mask(SDL_Surface *src, int sx, int sy, int dx ,int dy, int w, int h, uint16_t val) {
uint8_t *sp, *dp;
int x, y;
assert(src->format->Amask == 0xff000000);
assert(sdl_surface_alpha_mask(src) == 0xff000000);
dp = PIXEL_AT(sact.dmap, dx, dy);
sp = ALPHA_AT(src, sx, sy);
@@ -75,7 +75,7 @@ void sp_draw(sprite_t *sp) {
return;
}
if (SDL_ISPIXELFORMAT_ALPHA(cg->sf->format->format) || sp->blendrate < 255) {
if (SDL_ISPIXELFORMAT_ALPHA(sdl_surface_format(cg->sf)) || sp->blendrate < 255) {
SDL_SetSurfaceBlendMode(cg->sf, SDL_BLENDMODE_BLEND);
SDL_SetSurfaceAlphaMod(cg->sf, sp->blendrate);
} else {
@@ -116,7 +116,7 @@ void sp_draw_dmap(void* data, void* userdata) {
return;
}
if (SDL_ISPIXELFORMAT_ALPHA(cg->sf->format->format)) {
if (SDL_ISPIXELFORMAT_ALPHA(sdl_surface_format(cg->sf))) {
fill_dmap_mask(cg->sf, sx, sy, dx, dy, w, h, sp->no);
} else {
SDL_FillRect(sact.dmap, &(SDL_Rect){dx, dy, w, h}, sp->no);
-5
View File
@@ -39,11 +39,6 @@
@param cancel: キー抜け(0:なし, 1:あり)
*/
void sp_eupdate(int type, int time, int cancel) {
if (sact.waitskiplv > 1) {
sp_update_all(true);
return;
}
sp_update_all(false);
enum effect_type effect = from_sact_effect(type);
-5
View File
@@ -426,11 +426,6 @@ static void cb_waitkey_backlog(agsevent_t *e) {
X|SDL のイベントディスパッチャからくる最初の場所
*/
void spev_callback(agsevent_t *e) {
// menu open中は無視
if (nact->popupmenu_opened) {
return;
}
if (sact.waittype != KEYWAIT_BACKLOG) {
if (e->type == AGSEVENT_KEY_PRESS && e->code == KEY_CTRL) {
sact.waitskiplv = 2;
+1 -2
View File
@@ -113,7 +113,7 @@ static bool waitcond(int endtime) {
@param vD03: タイムアウトした場合=1, しない場合=0
@param wTime: タイムアウト時間 (1/100sec)
*/
void sp_keywait(int *vOK, int *vRND, int *vD01, int *vD02, int *vD03, int timeout) {
void sp_keywait(vmvar_t *vOK, vmvar_t *vRND, vmvar_t *vD01, vmvar_t *vD02, vmvar_t *vD03, int timeout) {
int curtime, endtime;
// とりあえず全更新
@@ -171,4 +171,3 @@ void sp_keywait(int *vOK, int *vRND, int *vD01, int *vD02, int *vD03, int timeou
sact.waittype = KEYWAIT_NONE;
}
+18 -9
View File
@@ -30,9 +30,11 @@
#include "portab.h"
#include "system.h"
#include "ags.h"
#include "gfx.h"
#include "nact.h"
#include "variable.h"
#include "input.h"
#include "msgskip.h"
#include "sact.h"
#include "sprite.h"
#include "drawtext.h"
@@ -236,11 +238,12 @@ void smsg_newline(int wNum, int size) {
@param wRLineSpace: ルビと本文の文字間隔
@param vLength: ???
*/
void smsg_out(int wNum, int wSize, int wColorR, int wColorG, int wColorB, int wFont, int wSpeed, int wLineSpace, int wAlign, int wRSize, int wRFont, int wRLineSpace, int *wLength) {
void smsg_out(int wNum, int wSize, int wColorR, int wColorG, int wColorB, int wFont, int wSpeed, int wLineSpace, int wAlign, int wRSize, int wRFont, int wRLineSpace, vmvar_t *wLength) {
char *msg;
sprite_t *sp;
int len = 0; // 処理した文字数?
bool needupdate = false;
bool has_batched_output = false;
bool flush_batched_output = wSpeed > 0;
SDL_Rect uparea = {0,0,0,0};
// wRSize == 0 -> ルビ無し(SACT.MessageOutputからの呼出)
@@ -250,7 +253,7 @@ void smsg_out(int wNum, int wSize, int wColorR, int wColorG, int wColorB, int wF
if (!is_messagesprite(wNum)) return;
// MessageSkip中は文字送り速度を最大に
if (sact.waitskiplv > 1) wSpeed = 0;
if (sact.waitskiplv > 1 || msgskip_isSkipping()) wSpeed = 0;
// shortcut
sp = sact.sp[wNum];
@@ -302,7 +305,7 @@ void smsg_out(int wNum, int wSize, int wColorR, int wColorG, int wColorB, int wF
mbuf,
wColorR, wColorG, wColorB);
needupdate = true;
has_batched_output = true;
append_to_log(mbuf);
@@ -313,7 +316,7 @@ void smsg_out(int wNum, int wSize, int wColorR, int wColorG, int wColorB, int wF
cw,
wSize + wRSize + wRLineSpace);
sp_update_clipped();
needupdate = false;
has_batched_output = false;
// keywait
delta = sys_get_ticks() - wcnt;
@@ -333,11 +336,17 @@ void smsg_out(int wNum, int wSize, int wColorR, int wColorG, int wColorB, int wF
// バッファリング中の文字のクリア
sact.msgbuf[0] = '\0';
// Waitなしの出力は最後にupdate
if (needupdate) {
// Register the update area after rendering without waits.
if (has_batched_output) {
uparea.w = sp->width;
uparea.h = min(sp->height, uparea.y - sp->u.msg.dspcur.y + wLineSpace + wLineSpace + wRSize);
uparea.h = min(sp->height - uparea.y,
sp->u.msg.dspcur.y - uparea.y + wSize + wRSize + wRLineSpace);
sp_updateme_part(sp, uparea.x, uparea.y, uparea.w, uparea.h);
// Present now if waiting was skipped.
if (flush_batched_output) {
sp_update_clipped();
gfx_updateScreen();
}
}
// ????
@@ -408,7 +417,7 @@ int smsg_keywait(int wNum1, int wNum2, int msglen) {
struct markinfo minfo[6];
int i = 0, j, maxstep;
if (sact.waitskiplv > 0) {
if (sact.waitskiplv > 0 || msgskip_isSkipping()) {
sys_getInputInfo();
return 0;
}
+7 -6
View File
@@ -26,7 +26,7 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <SDL.h>
#include "sdl_compat.h"
#include "portab.h"
#include "nact.h"
@@ -140,14 +140,14 @@ static void cb_select_release(agsevent_t *e) {
}
static void draw_box(SDL_Surface *dst, int x, int y, int w, int h) {
SDL_FillRect(dst, &(SDL_Rect){x, y, w, h}, SDL_MapRGB(dst->format, 0, 0, 0));
SDL_FillRect(dst, &(SDL_Rect){x, y, w, h}, sdl_map_rgb(dst, 0, 0, 0));
SDL_Rect border_rects[4] = {
{x, y, w, 1},
{x, y + h - 1, w, 1},
{x, y, 1, h},
{x + w - 1, y, 1, h}
};
SDL_FillRects(dst, border_rects, 4, SDL_MapRGB(dst->format, 255, 255, 255));
SDL_FillRects(dst, border_rects, 4, sdl_map_rgb(dst, 255, 255, 255));
}
// 選択ウィンドを更新するときの callback
@@ -180,7 +180,8 @@ static void setup_selwindow() {
int i;
// 選択肢文字用 canvas
sact.sel.charcanvas = SDL_CreateRGBSurfaceWithFormat(0, sp->cg1->sf->w, sp->cg1->sf->h, 32, SDL_PIXELFORMAT_ARGB8888);
sact.sel.charcanvas = sdl_create_surface(
sp->cg1->sf->w, sp->cg1->sf->h, 32, SDL_PIXELFORMAT_ARGB8888);
SDL_SetSurfaceBlendMode(sact.sel.charcanvas, SDL_BLENDMODE_BLEND);
dt_setfont(sact.sel.font_type, sact.sel.font_size);
@@ -228,7 +229,7 @@ static void remove_selwindow() {
// 作業用 surface の削除
if (sact.sel.charcanvas)
SDL_FreeSurface(sact.sel.charcanvas);
sdl_destroy_surface(sact.sel.charcanvas);
sact.sel.charcanvas = NULL;
}
@@ -266,7 +267,7 @@ void ssel_init() {
void ssel_reset(void) {
ssel_clear();
if (sact.sel.charcanvas) {
SDL_FreeSurface(sact.sel.charcanvas);
sdl_destroy_surface(sact.sel.charcanvas);
sact.sel.charcanvas = NULL;
}
}
-7
View File
@@ -117,11 +117,4 @@ void spev_main() {
e.type = AGSEVENT_TIMER;
tevent_callback(&e);
// デフォルトのコールバックのうち、ここで必要なものだけ
// 処理。(VAコマンドcallbackはなし)
if (nact->popupmenu_opened) {
menu_gtkmainiteration();
if (nact->is_quit) sys_exit(0);
}
}
+83 -83
View File
@@ -43,10 +43,10 @@ static void GetAtArray(void) { /* 0 */
type: 演算の種類
vResult: 演算結果を返す変数
*/
int *vAry = getCaliVariable();
vmvar_t *vAry = getCaliVariable();
int cnt = getCaliValue();
int type = getCaliValue();
int *vResult = getCaliVariable();
vmvar_t *vResult = getCaliVariable();
int i, j;
TRACE("ShArray.GetAtArray %p,%d,%d,%p:", vAry, cnt, type, vResult);
@@ -88,8 +88,8 @@ static void AddAtArray(void) { /* 1 */
vAry2: 配列2
cnt : 個数
*/
int *vAry1 = getCaliVariable();
int *vAry2 = getCaliVariable();
vmvar_t *vAry1 = getCaliVariable();
vmvar_t *vAry2 = getCaliVariable();
int cnt = getCaliValue();
int i;
@@ -114,8 +114,8 @@ static void SubAtArray(void) { /* 2 */
vAry2: 配列2
cnt : 個数
*/
int *vAry1 = getCaliVariable();
int *vAry2 = getCaliVariable();
vmvar_t *vAry1 = getCaliVariable();
vmvar_t *vAry2 = getCaliVariable();
int cnt = getCaliValue();
int i;
@@ -140,8 +140,8 @@ static void MulAtArray(void) { /* 3 */
vAry2: 配列2
cnt : 個数
*/
int *vAry1 = getCaliVariable();
int *vAry2 = getCaliVariable();
vmvar_t *vAry1 = getCaliVariable();
vmvar_t *vAry2 = getCaliVariable();
int cnt = getCaliValue();
int i;
@@ -166,8 +166,8 @@ static void DivAtArray(void) { /* 4 */
vAry2: 配列2
cnt : 個数
*/
int *vAry1 = getCaliVariable();
int *vAry2 = getCaliVariable();
vmvar_t *vAry1 = getCaliVariable();
vmvar_t *vAry2 = getCaliVariable();
int cnt = getCaliValue();
int i;
@@ -196,8 +196,8 @@ static void MinAtArray(void) { /* 5 */
vAry2: 配列2
cnt : 個数
*/
int *vAry1 = getCaliVariable();
int *vAry2 = getCaliVariable();
vmvar_t *vAry1 = getCaliVariable();
vmvar_t *vAry2 = getCaliVariable();
int cnt = getCaliValue();
int i;
@@ -219,8 +219,8 @@ static void MaxAtArray(void) { /* 6 */
vAry2: 配列2
cnt : 個数
*/
int *vAry1 = getCaliVariable();
int *vAry2 = getCaliVariable();
vmvar_t *vAry1 = getCaliVariable();
vmvar_t *vAry2 = getCaliVariable();
int cnt = getCaliValue();
int i;
@@ -242,7 +242,7 @@ static void AndNumArray(void) { /* 7 */
cnt : 個数
val : ANDをとる値
*/
int *vAry = getCaliVariable();
vmvar_t *vAry = getCaliVariable();
int cnt = getCaliValue();
int val = getCaliValue();
int i;
@@ -263,7 +263,7 @@ static void OrNumArray(void) { /* 8 */
cnt : 個数
val : ORをとる値
*/
int *vAry = getCaliVariable();
vmvar_t *vAry = getCaliVariable();
int cnt = getCaliValue();
int val = getCaliValue();
int i;
@@ -284,7 +284,7 @@ static void XorNumArray(void) { /* 9 */
cnt : 個数
val : XORをとる値
*/
int *vAry = getCaliVariable();
vmvar_t *vAry = getCaliVariable();
int cnt = getCaliValue();
int val = getCaliValue();
int i;
@@ -307,10 +307,10 @@ static void SetEquArray(void) { /* 10 */
val : 比較する値
vResults : 結果を格納する配列
*/
int *vAry = getCaliVariable();
vmvar_t *vAry = getCaliVariable();
int cnt = getCaliValue();
int val = getCaliValue();
int *vResults = getCaliVariable();
vmvar_t *vResults = getCaliVariable();
int i;
TRACE("ShArray.SetEquArray %p,%d,%d,%p:", vAry, cnt, val, vResults);
@@ -331,10 +331,10 @@ static void SetNotArray(void) { /* 11 */
val : 比較する値
vResults : 結果を格納する配列
*/
int *vAry = getCaliVariable();
vmvar_t *vAry = getCaliVariable();
int cnt = getCaliValue();
int val = getCaliValue();
int *vResults = getCaliVariable();
vmvar_t *vResults = getCaliVariable();
int i;
TRACE("ShArray.SetNotArray %p,%d,%d,%p:", vAry, cnt, val, vResults);
@@ -354,10 +354,10 @@ static void SetLowArray(void) { /* 12 */
val : 閾値
vResult: 結果を返す変数
*/
int *vAry = getCaliVariable();
vmvar_t *vAry = getCaliVariable();
int cnt = getCaliValue();
int val = getCaliValue();
int *vResults = getCaliVariable();
vmvar_t *vResults = getCaliVariable();
int i;
TRACE("ShArray.SetLowArray %p,%d,%d,%p:", vAry, cnt, val, vResults);
@@ -377,10 +377,10 @@ static void SetHighArray(void) { /* 13 */
val : 閾値
vResults: 結果を返す変数
*/
int *vAry = getCaliVariable();
vmvar_t *vAry = getCaliVariable();
int cnt = getCaliValue();
int val = getCaliValue();
int *vResults = getCaliVariable();
vmvar_t *vResults = getCaliVariable();
int i;
TRACE("ShArray.SetHighArray %p,%d,%d,%p:", vAry, cnt, val, vResults);
@@ -403,11 +403,11 @@ static void SetRangeArray(void) { /* 14 */
min < vAry < max の時 vResults = 1;
それ以外 vResults = 0;
*/
int *vAry = getCaliVariable();
vmvar_t *vAry = getCaliVariable();
int cnt = getCaliValue();
int min = getCaliValue();
int max = getCaliValue();
int *vResults = getCaliVariable();
vmvar_t *vResults = getCaliVariable();
int i;
TRACE("ShArray.SetRangeArray %p,%d,%d,%d,%p:", vAry, cnt, min, max, vResults);
@@ -430,11 +430,11 @@ static void SetAndEquArray(void) { /* 15 */
vResults : 結果を代入する配列
*/
int *vAry = getCaliVariable();
vmvar_t *vAry = getCaliVariable();
int mask = getCaliValue();
int cnt = getCaliValue();
int val = getCaliValue();
int *vResults = getCaliVariable();
vmvar_t *vResults = getCaliVariable();
int i;
TRACE("ShArray.SetAndEquArray: %p,%d,%d,%d,%p:", vAry, mask, cnt, val, vResults);
@@ -455,10 +455,10 @@ static void AndEquArray(void) { /* 16 */
val : 比較する値
vResults : 結果を代入する配列
*/
int *vAry = getCaliVariable();
vmvar_t *vAry = getCaliVariable();
int cnt = getCaliValue();
int val = getCaliValue();
int *vResults = getCaliVariable();
vmvar_t *vResults = getCaliVariable();
int i;
TRACE("ShArray.AndEquArray %p,%d,%d,%p:", vAry, cnt, val, vResults);
@@ -479,10 +479,10 @@ static void AndNotArray(void) { /* 17 */
val : 比較する値
vResults : 結果を代入する配列
*/
int *vAry = getCaliVariable();
vmvar_t *vAry = getCaliVariable();
int cnt = getCaliValue();
int val = getCaliValue();
int *vResults = getCaliVariable();
vmvar_t *vResults = getCaliVariable();
int i;
TRACE("ShArray.AndNotArray %p,%d,%d,%p:", vAry, cnt, val, vResults);
@@ -503,10 +503,10 @@ static void AndLowArray(void) { /* 18 */
min : 最小値
vResults : 結果を代入する配列
*/
int *vAry = getCaliVariable();
vmvar_t *vAry = getCaliVariable();
int cnt = getCaliValue();
int min = getCaliValue();
int *vResults = getCaliVariable();
vmvar_t *vResults = getCaliVariable();
int i;
TRACE("ShArray.AndLowArray: %d,%d,%d,%d:", vAry, cnt, min, vResults);
@@ -527,10 +527,10 @@ static void AndHighArray(void) { /* 19 */
max : 最小値
vResults : 結果を代入する配列
*/
int *vAry = getCaliVariable();
vmvar_t *vAry = getCaliVariable();
int cnt = getCaliValue();
int max = getCaliValue();
int *vResults = getCaliVariable();
vmvar_t *vResults = getCaliVariable();
int i;
TRACE("ShArray.AndHighArray: %p,%d,%d,%p:", vAry, cnt, max, vResults);
@@ -552,11 +552,11 @@ static void AndRangeArray(void) { /* 20 */
max: 最大値
vResults: 結果を返す配列
*/
int *vAry = getCaliVariable();
vmvar_t *vAry = getCaliVariable();
int cnt = getCaliValue();
int min = getCaliValue();
int max = getCaliValue();
int *vResults = getCaliVariable();
vmvar_t *vResults = getCaliVariable();
int i;
TRACE("ShArray.AndRangeArray %d,%d,%d,%d,%d:", vAry, cnt, min, max, vResults);
@@ -579,11 +579,11 @@ static void AndAndEquArray(void) { /* 21 */
vResults : 結果を代入する配列
*/
int *vAry = getCaliVariable();
vmvar_t *vAry = getCaliVariable();
int mask = getCaliValue();
int cnt = getCaliValue();
int val = getCaliValue();
int *vResults = getCaliVariable();
vmvar_t *vResults = getCaliVariable();
int i;
TRACE("ShArray.AndAndEquArray: %d,%d,%d,%d,%d:", vAry, mask, cnt, val, vResults);
@@ -612,10 +612,10 @@ static void OrNotArray(void) { /* 23 */
val : 比較する値
vResults: 結果を書き込む変数
*/
int *vAry = getCaliVariable();
vmvar_t *vAry = getCaliVariable();
int cnt = getCaliValue();
int val = getCaliValue();
int *vResults = getCaliVariable();
vmvar_t *vResults = getCaliVariable();
int i;
TRACE("ShArray.OrNotArray %p,%d,%d,%p:", vAry, cnt, val, vResults);
@@ -674,10 +674,10 @@ static void EnumEquArray(void) { /* 28 */
val : 比較する値
vResult: 一致する個数を返す変数
*/
int *vAry = getCaliVariable();
vmvar_t *vAry = getCaliVariable();
int cnt = getCaliValue();
int val = getCaliValue();
int *vResult = getCaliVariable();
vmvar_t *vResult = getCaliVariable();
int i;
TRACE("ShArray.EnumEquArray %p,%d,%d,%p:", vAry, cnt, val, vResult);
@@ -713,12 +713,12 @@ static void EnumEquNotArray2(void) { /* 30 */
val2: 配列2と比較する値
vResult: 条件に一致する数を返す変数
*/
int *vAry1 = getCaliVariable();
int *vAry2 = getCaliVariable();
vmvar_t *vAry1 = getCaliVariable();
vmvar_t *vAry2 = getCaliVariable();
int cnt = getCaliValue();
int val1 = getCaliValue();
int val2 = getCaliValue();
int *vResult = getCaliVariable();
vmvar_t *vResult = getCaliVariable();
int i;
TRACE("ShArray.EnumEquNotArray2 %p,%p,%d,%d,%d,%p:", vAry1, vAry2, cnt, val1, val2, vResult);
@@ -742,10 +742,10 @@ static void EnumNotArray(void) { /* 31 */
val: 比較する値
vResult: 等しくないものの数
*/
int *vAry = getCaliVariable();
vmvar_t *vAry = getCaliVariable();
int cnt = getCaliValue();
int val = getCaliValue();
int *vResult = getCaliVariable();
vmvar_t *vResult = getCaliVariable();
int i;
TRACE("ShArray.EnumNotArray %p, %d, %d, %p:", vAry, cnt, val, vResult);
@@ -799,11 +799,11 @@ static void EnumRangeArray(void) { /* 35 */
max : 最大値
vResult: 一致した数を返す変数
*/
int *vAry = getCaliVariable();
vmvar_t *vAry = getCaliVariable();
int cnt = getCaliValue();
int min = getCaliValue();
int max = getCaliValue();
int *vResult = getCaliVariable();
vmvar_t *vResult = getCaliVariable();
int i;
TRACE("ShArray.EnumRangeArray %d,%d,%d,%d,%d:", vAry, cnt, min, max, vResult);
@@ -830,11 +830,11 @@ static void GrepEquArray(void) { /* 36 */
vMatch: 一致したインデックス
vResult: 一つでも val と同じ値があれば 1
*/
int *vAry = getCaliVariable();
vmvar_t *vAry = getCaliVariable();
int cnt = getCaliValue();
int val = getCaliValue();
int *vMatch = getCaliVariable();
int *vResult = getCaliVariable();
vmvar_t *vMatch = getCaliVariable();
vmvar_t *vResult = getCaliVariable();
int i;
TRACE("ShArray.GrepEquArray %p,%d,%d,%p,%p:", vAry, cnt, val, vMatch, vResult);
@@ -862,11 +862,11 @@ static void GrepNotArray(void) { /* 37 */
vMatch: 一致するindex
vResult: 一つでも val と同じ値があれば 1
*/
int *vAry = getCaliVariable();
vmvar_t *vAry = getCaliVariable();
int cnt = getCaliValue();
int val = getCaliValue();
int *vMatch = getCaliVariable();
int *vResult = getCaliVariable();
vmvar_t *vMatch = getCaliVariable();
vmvar_t *vResult = getCaliVariable();
int i;
TRACE("ShArray.GrepNotArray %p,%d,%d,%p,%p:", vAry, cnt, val, vMatch, vResult);
@@ -930,11 +930,11 @@ static void GrepLowArray(void) { /* 41 */
vMatch: 一致したインデックス
vResult: 一つでも val と同じ値があれば 1
*/
int *vAry = getCaliVariable();
vmvar_t *vAry = getCaliVariable();
int cnt = getCaliValue();
int min = getCaliValue();
int *vMatch = getCaliVariable();
int *vResult = getCaliVariable();
vmvar_t *vMatch = getCaliVariable();
vmvar_t *vResult = getCaliVariable();
int i;
TRACE("ShArray.GrepLowArray: %p,%d,%d,%p,%p:", vAry, cnt, min, vMatch, vResult);
@@ -962,11 +962,11 @@ static void GrepHighArray(void) { /* 42 */
vMatch: 一致したインデックス
vResult: 一つでも val と同じ値があれば 1
*/
int *vAry = getCaliVariable();
vmvar_t *vAry = getCaliVariable();
int cnt = getCaliValue();
int max = getCaliValue();
int *vMatch = getCaliVariable();
int *vResult = getCaliVariable();
vmvar_t *vMatch = getCaliVariable();
vmvar_t *vResult = getCaliVariable();
int i;
TRACE("ShArray.GrepHighArray: %p,%d,%d,%p,%p:", vAry, cnt, max, vMatch, vResult);
@@ -995,12 +995,12 @@ static void GrepRangeArray(void) { /* 43 */
vMatch: 一致したインデックス
vResult: 一つでも val と同じ値があれば 1
*/
int *vAry = getCaliVariable();
vmvar_t *vAry = getCaliVariable();
int cnt = getCaliValue();
int min = getCaliValue();
int max = getCaliValue();
int *vMatch = getCaliVariable();
int *vResult = getCaliVariable();
vmvar_t *vMatch = getCaliVariable();
vmvar_t *vResult = getCaliVariable();
int i;
TRACE("ShArray.GrepRangeArray %p,%d,%d,%d,%p,%p:", vAry, cnt, max, min, vMatch, vResult);
@@ -1031,13 +1031,13 @@ static void GrepLowOrderArray(void) { /* 44 */
vLastMatch: 最小値を示す配列のindex
vResult: 最小値が見つかれば 1, 見つからなければ 0
*/
int *vAry = getCaliVariable();
vmvar_t *vAry = getCaliVariable();
int cnt = getCaliValue();
int min = getCaliValue();
int max = getCaliValue();
int *v1 = getCaliVariable();
int *vLastMatch = getCaliVariable();
int *vResult = getCaliVariable();
vmvar_t *v1 = getCaliVariable();
vmvar_t *vLastMatch = getCaliVariable();
vmvar_t *vResult = getCaliVariable();
int i, j, k = 0;
TRACE("ShArray.GrepLowOrderArray %p,%d,%d,%d,%p,%p,%p:", vAry, cnt, min, max, v1, vLastMatch, vResult);
@@ -1082,13 +1082,13 @@ static void GrepHighOrderArray(void) { /* 45 */
vLastMatch: 最大値を示す配列のindex
vResult: 最大値が見つかれば 1, 見つからなければ 0
*/
int *vAry = getCaliVariable();
vmvar_t *vAry = getCaliVariable();
int cnt = getCaliValue();
int min = getCaliValue();
int max = getCaliValue();
int *v1 = getCaliVariable();
int *vLastMatch = getCaliVariable();
int *vResult = getCaliVariable();
vmvar_t *v1 = getCaliVariable();
vmvar_t *vLastMatch = getCaliVariable();
vmvar_t *vResult = getCaliVariable();
int i, j, k = 0;
TRACE("ShArray.GrepHighOrderArray %p,%d,%d,%d,%p,%p,%p:", vAry, cnt, min, max, v1, vLastMatch, vResult);
@@ -1119,7 +1119,7 @@ static void GrepHighOrderArray(void) { /* 45 */
}
static void ChangeEquArray(void) { /* 46 */
int *vAry = getCaliVariable();
vmvar_t *vAry = getCaliVariable();
int cnt = getCaliValue();
int src = getCaliValue();
int dst = getCaliValue();
@@ -1172,7 +1172,7 @@ static void ChangeRangeArray(void) { /* 50 */
max : 最大値
val : 置き換える値
*/
int *vAry = getCaliVariable();
vmvar_t *vAry = getCaliVariable();
int cnt = getCaliValue();
int min = getCaliValue();
int max = getCaliValue();
@@ -1202,12 +1202,12 @@ static void CopyArrayToRect(void) { /* 51 */
dw : コピー先 width
dh : コピー先 height
*/
int *vSrc = getCaliVariable();
vmvar_t *vSrc = getCaliVariable();
int sw = getCaliValue();
int sh = getCaliValue();
int sx = getCaliValue();
int sy = getCaliValue();
int *vDst = getCaliVariable();
vmvar_t *vDst = getCaliVariable();
int dw = getCaliValue();
int dh = getCaliValue();
int x, y;
@@ -1236,10 +1236,10 @@ static void CopyRectToArray(void) { /* 52 */
dx : コピー先 x
dy : コピー先 y
*/
int *vSrc = getCaliVariable();
vmvar_t *vSrc = getCaliVariable();
int sw = getCaliValue();
int sh = getCaliValue();
int *vDst = getCaliVariable();
vmvar_t *vDst = getCaliVariable();
int dw = getCaliValue();
int dh = getCaliValue();
int dx = getCaliValue();
@@ -1266,10 +1266,10 @@ static void ChangeSecretArray(void) { /* 53 */
type: 機能番号
vResult: 結果を返す変数
*/
int *vAry = getCaliVariable();
vmvar_t *vAry = getCaliVariable();
int cnt = getCaliValue();
int type = getCaliValue();
int *vResult = getCaliVariable();
vmvar_t *vResult = getCaliVariable();
static uint16_t key[4] = { 0x7A7A, 0xADAD, 0xBCBC, 0xCECE }; /* key */
TRACE("ShArray.ChangeSecretArray %p,%d,%d,%p:", vAry, cnt, type, vResult);
+19 -19
View File
@@ -50,7 +50,7 @@ static int64_t div64(int64_t a1, int64_t a2) {
return a1 / a2;
}
static int64_t get32(int *var) {
static int64_t get32(vmvar_t *var) {
return var[0] + mul64(var[1], 0x10000);
}
@@ -74,7 +74,7 @@ static void SetIntNum16(void) { /* 1 */
var:
*/
int *var = getCaliVariable();
vmvar_t *var = getCaliVariable();
accumulator = mul64(*var, numbase);
@@ -82,7 +82,7 @@ static void SetIntNum16(void) { /* 1 */
}
static void SetIntNum32(void) { /* 2 */
int *var = getCaliVariable();
vmvar_t *var = getCaliVariable();
accumulator = mul64(get32(var), numbase);
@@ -95,7 +95,7 @@ static void GetIntNum16(void) { /* 3 */
var:
*/
int *var = getCaliVariable();
vmvar_t *var = getCaliVariable();
int64_t i;
i = div64(accumulator, numbase);
@@ -110,7 +110,7 @@ static void GetIntNum16(void) { /* 3 */
}
static void GetIntNum32(void) { /* 4 */
int *var = getCaliVariable();
vmvar_t *var = getCaliVariable();
int64_t i = div64(accumulator, numbase);
var[0] = i & 0xFFFF;
@@ -126,7 +126,7 @@ static void AddIntNum16(void) { /* 5 */
var:
*/
int *var = getCaliVariable();
vmvar_t *var = getCaliVariable();
accumulator += mul64(*var, numbase);
@@ -134,7 +134,7 @@ static void AddIntNum16(void) { /* 5 */
}
static void AddIntNum32(void) { /* 6 */
int *var = getCaliVariable();
vmvar_t *var = getCaliVariable();
accumulator += mul64(get32(var), numbase);
@@ -148,7 +148,7 @@ static void SubIntNum16(void) { /* 7 */
}
static void SubIntNum32(void) { /* 8 */
int *var = getCaliVariable();
vmvar_t *var = getCaliVariable();
accumulator -= mul64(get32(var), numbase);
@@ -161,7 +161,7 @@ static void MulIntNum16(void) { /* 9 */
var:
*/
int *var = getCaliVariable();
vmvar_t *var = getCaliVariable();
accumulator *= mul64(*var, numbase);
@@ -180,7 +180,7 @@ static void DivIntNum16(void) { /* 11 */
var:
*/
int *var = getCaliVariable();
vmvar_t *var = getCaliVariable();
int64_t i;
i = mul64(*var, numbase);
@@ -204,9 +204,9 @@ static void CmpIntNum16(void) { /* 13 */
}
static void CmpIntNum32(void) { /* 14 */
int *var = getCaliVariable();
vmvar_t *var = getCaliVariable();
int op = getCaliValue();
int *result = getCaliVariable();
vmvar_t *result = getCaliVariable();
int64_t val = mul64(get32(var), numbase);
@@ -230,8 +230,8 @@ static void GetLengthNum16(void) { /* 15 */
var:
vResult:
*/
int *var = getCaliVariable();
int *vResult = getCaliVariable();
vmvar_t *var = getCaliVariable();
vmvar_t *vResult = getCaliVariable();
if (*var >= 10000) {
*vResult = 5;
@@ -270,7 +270,7 @@ static void NumToRate(void) { /* 17 */
int p2 = getCaliValue();
int p3 = getCaliValue();
int flag = getCaliValue();
int *vResult = getCaliVariable();
vmvar_t *vResult = getCaliVariable();
int i;
i = (p1 * p3) / p2;
@@ -302,7 +302,7 @@ static void NumToRateNum(void) { /* 18 */
int p2 = getCaliValue();
int p3 = getCaliValue();
int flag = getCaliValue();
int *vResult = getCaliVariable();
vmvar_t *vResult = getCaliVariable();
int i;
i = (p1 * p2) / p3;
@@ -333,7 +333,7 @@ static void SetRandomSeed() {
static void GetRandomNumA() {
int num = getCaliValue();
int *var = getCaliVariable();
vmvar_t *var = getCaliVariable();
if (num == 0 || num == 1) {
*var = num;
@@ -351,7 +351,7 @@ static void NumToBit() {
var:
*/
int beki = getCaliValue();
int *var = getCaliVariable();
vmvar_t *var = getCaliVariable();
int i, j = 1;
if (beki < 17) {
@@ -379,7 +379,7 @@ static void BitToNum() {
var:
*/
int val = getCaliValue();
int *var = getCaliVariable();
vmvar_t *var = getCaliVariable();
int i;
TRACE("ShCalc.BitToNum %d,%p:", val, var);
+14 -14
View File
@@ -59,8 +59,8 @@ struct animsrc {
static struct animsrc src[SLOT]; /* アニメーション各コマ転送元 */
struct _s0 {
int *dst_p1;
int *dst_p2;
vmvar_t *dst_p1;
vmvar_t *dst_p2;
int dw_1000A188;
};
static struct _s0 s0[SLOT];
@@ -84,15 +84,15 @@ struct _s2 {
};
static struct _s2 s2[SLOT];
static int* add_p5[SLOT]; /* どこまでアニメーションのコマが進んだか */
static vmvar_t *add_p5[SLOT]; /* どこまでアニメーションのコマが進んだか */
static void copy_sprite(int sx, int sy, int width, int height, int dx, int dy, int r, int g, int b) {
SDL_Surface *sf = nact->ags.dib->sdl_surface;
SDL_SetColorKey(sf, SDL_TRUE, SDL_MapRGB(sf->format, r, g, b));
sdl_set_surface_color_key(sf, true, sdl_map_rgb(sf, r, g, b));
SDL_Rect src_rect = { sx, sy, width, height };
SDL_Rect dst_rect = { dx, dy, width, height };
SDL_BlitSurface(sf, &src_rect, sf, &dst_rect);
SDL_SetColorKey(sf, SDL_FALSE, 0);
sdl_set_surface_color_key(sf, false, 0);
}
static void Init() {
@@ -115,8 +115,8 @@ static void ChangeEquColor() {
int p2 = getCaliValue();
int p3 = getCaliValue();
int p4 = getCaliValue();
int *p5 = getCaliVariable();
int *p6 = getCaliVariable();
vmvar_t *p5 = getCaliVariable();
vmvar_t *p6 = getCaliVariable();
int p7 = getCaliValue(); /* ISurface */
TRACE_UNIMPLEMENTED("ShGraph.ChangeEquColor %d,%d,%d,%d,%p,%p,%d:", p1, p2, p3, p4, p5, p6, p7);
@@ -137,8 +137,8 @@ static void ChangeNotColor() {
int y0 = getCaliValue();
int width = getCaliValue();
int height = getCaliValue();
int *src = getCaliVariable(); /* r, g, b */
int *dst = getCaliVariable(); /* r, g, b */
vmvar_t *src = getCaliVariable(); /* r, g, b */
vmvar_t *dst = getCaliVariable(); /* r, g, b */
int p7 = getCaliValue(); /* ISurface */
surface_t *dib;
int x, y;
@@ -216,7 +216,7 @@ static void ResetAnimeData() {
}
memset(s1, 0, sizeof(struct _s1) * SLOT);
memset(add_p5, 0, sizeof(int *) * SLOT);
memset(add_p5, 0, sizeof(vmvar_t *) * SLOT);
}
@@ -240,7 +240,7 @@ static void SetAnimeSrc() {
int h = getCaliValue();
int uw = getCaliValue();
int uh = getCaliValue();
int *pal = getCaliVariable();
vmvar_t *pal = getCaliVariable();
int r, g, b;
TRACE("ShGraph.SetAnimeSrc %d,%d,%d,%d,%d,%d,%d,%p:", no, x0, y0, w, h, uw, uh, pal);
@@ -276,8 +276,8 @@ static void SetAnimeDst() {
p6: (h) 10000
*/
int no = getCaliValue();
int *p1 = getCaliVariable();
int *p2 = getCaliVariable();
vmvar_t *p1 = getCaliVariable();
vmvar_t *p2 = getCaliVariable();
int p3 = getCaliValue();
int p4 = getCaliValue();
int p5 = getCaliValue();
@@ -313,7 +313,7 @@ static void AddAnimeData() {
int p2 = getCaliValue();
int p3 = getCaliValue();
int p4 = getCaliValue();
int *p5 = getCaliVariable();
vmvar_t *p5 = getCaliVariable();
int p6 = getCaliValue();
int i;
+8 -9
View File
@@ -36,7 +36,7 @@
#include "xsystem35.h"
#include "modules.h"
#include "input.h"
#include "menu.h"
#include "input_modal.h"
// キー変換テーブル
#define KEYMAP_MAX 8
@@ -47,7 +47,7 @@ static void OutputMessageBox(void) { /* 0 */
int p2 = getCaliValue();
int title = getCaliValue();
int msg = getCaliValue();
int *res = getCaliVariable();
vmvar_t *res = getCaliVariable();
int ISys3xSystem = getCaliValue();
char *title_utf8 = toUTF8(svar_get(title));
@@ -64,10 +64,10 @@ static void OutputMessageBox(void) { /* 0 */
static void InputListNum(void) { /* 1 */
int flags = getCaliValue();
int title = getCaliValue();
int *val = getCaliVariable();
vmvar_t *val = getCaliVariable();
int minval = getCaliValue();
int maxval = getCaliValue();
int *res = getCaliVariable();
vmvar_t *res = getCaliVariable();
int ISys3xSystem = getCaliValue();
INPUTNUM_PARAM ni_param = {
@@ -77,12 +77,11 @@ static void InputListNum(void) { /* 1 */
.title = toUTF8(svar_get(title)),
};
menu_inputnumber(&ni_param);
if (ni_param.value < 0) {
*res = 0;
} else {
if (input_modal_number(&ni_param)) {
*val = (uint16_t)ni_param.value;
*res = 1;
} else {
*res = 0;
}
free(ni_param.title);
@@ -152,7 +151,7 @@ static void SetKeyStatus(void) {
*/
static void GetKeyStatus(void) {
int no = getCaliValue();
int *var = getCaliVariable();
vmvar_t *var = getCaliVariable();
int i;
if (no >= KEYMAP_MAX) {
+12 -7
View File
@@ -25,8 +25,9 @@
#include "config.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <SDL.h>
#include "sdl_compat.h"
#include "portab.h"
#include "LittleEndian.h"
@@ -44,7 +45,7 @@ static struct {
} memwav;
static void free_memory_wav(void) {
SDL_FreeWAV(memwav.buf);
sdl_free_wav(memwav.buf);
memwav.buf = NULL;
}
@@ -132,11 +133,15 @@ static void wavLoadMemory() {
WARNING("cannot open WAVE %d", no - 1);
return;
}
if (!SDL_LoadWAV_RW(SDL_RWFromConstMem(dfile->data, dfile->size), 1, &memwav.spec, &memwav.buf, &memwav.len)) {
sdl_iostream_t *stream = sdl_io_from_const_memory(
dfile->data, dfile->size);
if (!stream || !sdl_load_wav_io(
stream, true, &memwav.spec, &memwav.buf, &memwav.len)) {
WARNING("cannot load WAVE %d", no - 1);
return;
}
if (memwav.spec.channels != 2 || memwav.spec.format != AUDIO_S16LSB) {
if (memwav.spec.channels != 2 ||
memwav.spec.format != sdl_audio_s16le_format()) {
WARNING("unexpected audio format");
free_memory_wav();
return;
@@ -268,7 +273,7 @@ static void wavPlayRing() {
*/
int start = getCaliValue();
int cnt = getCaliValue();
int *cur = getCaliVariable();
vmvar_t *cur = getCaliVariable();
mus_wav_play(start + (*cur % cnt), 1);
*cur = (*cur + 1) % cnt;
@@ -321,7 +326,7 @@ static void wavIsPlay() {
*result: 0!0
*/
int slot = getCaliValue();
int *result = getCaliVariable();
vmvar_t *result = getCaliVariable();
*result = mus_wav_get_playposition(slot);
@@ -338,7 +343,7 @@ static void wavIsPlayRange() {
*/
int slot = getCaliValue();
int range = getCaliValue();
int *result = getCaliVariable();
vmvar_t *result = getCaliVariable();
int i, ret = 0;
for (i = slot; i < (slot + range); i++) {
+2 -2
View File
@@ -127,7 +127,7 @@ static void SetStringNum16(void) {
p2:
*/
int st = getCaliValue();
int *var = getCaliVariable();
vmvar_t *var = getCaliVariable();
const char *str = svar_get(st);
char _dst[100];
char *dst = _dst;
@@ -160,7 +160,7 @@ static void SetStringNum16(void) {
static void SetStringNum32(void) {
int p1 = getCaliValue();
int *p2 = getCaliVariable();
vmvar_t *p2 = getCaliVariable();
TRACE_UNIMPLEMENTED("ShString.SetStringNum32: %d,%p:", p1, p2);
}
+60 -54
View File
@@ -2,7 +2,7 @@
#include <stdio.h>
#include <math.h>
#include <SDL.h>
#include "sdl_compat.h"
#include "portab.h"
#include "system.h"
@@ -31,7 +31,7 @@ static void render_quad(SDL_Texture *tex, SDL_Vertex verts[4]) {
static void fade_overlay(Uint8 r, Uint8 g, Uint8 b, Uint8 alpha) {
SDL_SetRenderDrawBlendMode(gfx_renderer, SDL_BLENDMODE_BLEND);
SDL_SetRenderDrawColor(gfx_renderer, r, g, b, alpha);
SDL_RenderFillRect(gfx_renderer, NULL);
sdl_render_fill_rect(gfx_renderer, NULL);
}
#define NR_SCENES 13
@@ -94,7 +94,7 @@ static void Init() {
int p1 = getCaliValue();
int p2 = getCaliValue();
int p3 = getCaliValue();
int *var = getCaliVariable();
vmvar_t *var = getCaliVariable();
if (!nact->files.alk[0]) {
WARNING("dDEMO.alk not found");
@@ -121,7 +121,7 @@ static void Init() {
continue;
}
dd.images[i] = SDL_CreateTextureFromSurface(gfx_renderer, sf);
SDL_FreeSurface(sf);
sdl_destroy_surface(sf);
}
*var = 1;
TRACE("dDemo.Init %d,%d,%d,%p:", p1, p2, p3, var);
@@ -144,7 +144,7 @@ static void render_affine_bg(SDL_Texture *scroll_bg, int elapsed, int duration,
if (!scroll_bg) return;
int bg_w, bg_h;
SDL_QueryTexture(scroll_bg, NULL, NULL, &bg_w, &bg_h);
sdl_get_texture_size(scroll_bg, &bg_w, &bg_h);
int xs[4], ys[4];
const float angle = angle_deg * (float)M_PI / 180.0f;
@@ -183,7 +183,7 @@ static void render_affine_bg(SDL_Texture *scroll_bg, int elapsed, int duration,
for (int i = 0; i < 4; i++) {
verts[i].position.x = (i == 1 || i == 2) ? w : 0;
verts[i].position.y = (i == 2 || i == 3) ? h : (float)y_toggle;
verts[i].color = (SDL_Color){255, 255, 255, 255};
verts[i].color = sdl_vertex_color(255, 255, 255, 255);
verts[i].tex_coord.x = uv[i][0];
verts[i].tex_coord.y = uv[i][1];
}
@@ -194,7 +194,7 @@ static void render_affine_bg(SDL_Texture *scroll_bg, int elapsed, int duration,
y_toggle = (y_toggle + 1) & 1;
for (int i = 0; i < 4; i++) {
verts[i].position.y = (i == 2 || i == 3) ? h : (float)y_toggle;
verts[i].color = (SDL_Color){255, 255, 255, 128};
verts[i].color = sdl_vertex_color(255, 255, 255, 128);
}
// Swap mapping: TL<->BR, TR<->BL for 180° rotation effect
verts[0].tex_coord.x = uv[2][0]; verts[0].tex_coord.y = uv[2][1];
@@ -212,7 +212,7 @@ static void render_scene0(int elapsed) {
if (!sprite) return;
int spr_w, spr_h;
SDL_QueryTexture(sprite, NULL, NULL, &spr_w, &spr_h);
sdl_get_texture_size(sprite, &spr_w, &spr_h);
const int slide_end = 4700;
const int duration = scene_duration[0]; // 5400
@@ -234,12 +234,12 @@ static void render_scene0(int elapsed) {
int dest_w = a0 - ((a0 - spr_w) * elapsed) / slide_end;
if (dest_w <= 0) continue;
SDL_Rect dst = {view_w - 2 * dest_w, dest_y, dest_w, spr_h};
SDL_RenderCopy(gfx_renderer, sprite, NULL, &dst);
sdl_render_texture(gfx_renderer, sprite, NULL, &dst);
}
} else {
SDL_SetTextureAlphaMod(sprite, 0xd0);
SDL_Rect dst = {view_w - 2 * spr_w, dest_y, spr_w, spr_h};
SDL_RenderCopy(gfx_renderer, sprite, NULL, &dst);
sdl_render_texture(gfx_renderer, sprite, NULL, &dst);
}
// 3. Horizontal accent line (grows rightward over 4700ms)
@@ -247,12 +247,12 @@ static void render_scene0(int elapsed) {
SDL_SetRenderDrawBlendMode(gfx_renderer, SDL_BLENDMODE_NONE);
SDL_SetRenderDrawColor(gfx_renderer, 0xc0, 0xc0, 0xc0, 0xff);
SDL_Rect hline = {0, (spr_h + view_h) / 2 - 4, line_w, 2};
SDL_RenderFillRect(gfx_renderer, &hline);
sdl_render_fill_rect(gfx_renderer, &hline);
// 4. Vertical accent line (right edge of sprite, grows downward)
int line_h = elapsed < slide_end ? 479 * elapsed / slide_end + 1 : 480;
SDL_Rect vline = {view_w - spr_w, 0, 2, line_h};
SDL_RenderFillRect(gfx_renderer, &vline);
sdl_render_fill_rect(gfx_renderer, &vline);
}
static void render_scene1(int elapsed) {
@@ -261,7 +261,7 @@ static void render_scene1(int elapsed) {
if (!sprite) return;
int spr_w, spr_h;
SDL_QueryTexture(sprite, NULL, NULL, &spr_w, &spr_h);
sdl_get_texture_size(sprite, &spr_w, &spr_h);
const int duration = scene_duration[1]; // 4752
// --- Background: DrawAffineSurfaceAlpha50 x2 (alpha=50%) ---
@@ -275,7 +275,7 @@ static void render_scene1(int elapsed) {
SDL_SetTextureBlendMode(sprite, SDL_BLENDMODE_BLEND);
SDL_SetTextureAlphaMod(sprite, (Uint8)alpha);
SDL_Rect dst = {(view_w - spr_w) / 2, (view_h - spr_h) / 2, spr_w, spr_h};
SDL_RenderCopy(gfx_renderer, sprite, NULL, &dst);
sdl_render_texture(gfx_renderer, sprite, NULL, &dst);
}
// Fade to black: graph_FillAlphaColor(0,0,0), starts at 4000ms.
@@ -397,12 +397,14 @@ static void render_scene_2_4_6_3d(int elapsed, int phase_dur, SDL_Texture *tex1,
// Object border quad: solid color RGB(244,221,181) = BGR(0xb5,0xdd,0xf4),
// drawn first as a backdrop. The bg quad drawn on top leaves only the thin
// border ring visible.
for (int i = 0; i < 4; i++) obj_verts[i].color = (SDL_Color){0xf4, 0xdd, 0xb5, 0xff};
for (int i = 0; i < 4; i++)
obj_verts[i].color = sdl_vertex_color(0xf4, 0xdd, 0xb5, 0xff);
SDL_SetRenderDrawBlendMode(gfx_renderer, SDL_BLENDMODE_NONE);
render_quad(NULL, obj_verts);
// Background quad: textured, opaque, drawn on top (covers center of border quad)
for (int i = 0; i < 4; i++) bg_verts[i].color = (SDL_Color){255, 255, 255, 255};
for (int i = 0; i < 4; i++)
bg_verts[i].color = sdl_vertex_color(255, 255, 255, 255);
SDL_SetTextureBlendMode(tex, SDL_BLENDMODE_NONE);
render_quad(tex, bg_verts);
}
@@ -425,7 +427,7 @@ static void render_scene_2_4_6(int elapsed, int scene_idx, int sprite_idx, int t
if (bg) {
SDL_SetTextureBlendMode(bg, SDL_BLENDMODE_NONE);
SDL_SetTextureColorMod(bg, 0xff, 0xff, 0xff);
SDL_RenderCopy(gfx_renderer, bg, NULL, NULL);
sdl_render_texture(gfx_renderer, bg, NULL, NULL);
}
// 2. 3D rotating quad animation (8-phase timing)
@@ -439,23 +441,23 @@ static void render_scene_2_4_6(int elapsed, int scene_idx, int sprite_idx, int t
// 3. Scrolling sprite: x moves from -sprite_w (off-screen left) to view_w (off-screen right)
int sprite_w = 0, sprite_h = 0;
if (sprite)
SDL_QueryTexture(sprite, NULL, NULL, &sprite_w, &sprite_h);
sdl_get_texture_size(sprite, &sprite_w, &sprite_h);
int x = sprite_w ? ((sprite_w + view_w) * elapsed) / duration - sprite_w : 0;
// Shadow drawn first (color-modded to black), sprite on top (full color)
if (sprite_shadow) {
int sw, sh;
SDL_QueryTexture(sprite_shadow, NULL, NULL, &sw, &sh);
sdl_get_texture_size(sprite_shadow, &sw, &sh);
SDL_SetTextureBlendMode(sprite_shadow, SDL_BLENDMODE_BLEND);
SDL_SetTextureColorMod(sprite_shadow, 0, 0, 0);
SDL_Rect dst = {x, 0, sw, sh};
SDL_RenderCopy(gfx_renderer, sprite_shadow, NULL, &dst);
sdl_render_texture(gfx_renderer, sprite_shadow, NULL, &dst);
}
if (sprite) {
SDL_SetTextureBlendMode(sprite, SDL_BLENDMODE_BLEND);
SDL_SetTextureColorMod(sprite, 0xff, 0xff, 0xff);
SDL_Rect dst = {x, 0, sprite_w, sprite_h};
SDL_RenderCopy(gfx_renderer, sprite, NULL, &dst);
sdl_render_texture(gfx_renderer, sprite, NULL, &dst);
}
}
@@ -490,7 +492,7 @@ static void render_scene_3_5_7_9(int elapsed, int image_idx) {
if (alpha > 0) {
SDL_SetTextureBlendMode(main_img, alpha < 255 ? SDL_BLENDMODE_BLEND : SDL_BLENDMODE_NONE);
SDL_SetTextureAlphaMod(main_img, (Uint8)alpha);
SDL_RenderCopy(gfx_renderer, main_img, NULL, NULL);
sdl_render_texture(gfx_renderer, main_img, NULL, NULL);
}
}
@@ -500,10 +502,14 @@ static void render_scene_3_5_7_9(int elapsed, int image_idx) {
float t3 = (float)elapsed / (4000.0f * 3.0f); // 0 → 1/3
float w = (float)view_w, h = (float)view_h;
SDL_Vertex verts[4] = {
/* TL */ {{0.f, 0.f}, {255, 255, 255, 128}, {t3, 0.f }},
/* TR */ {{w, 0.f}, {255, 255, 255, 128}, {1.0f, t3 }},
/* BR */ {{w, h }, {255, 255, 255, 128}, {1.0f - t3, 1.0f }},
/* BL */ {{0.f, h }, {255, 255, 255, 128}, {0.f, 1.0f-t3}},
{{0.f, 0.f}, sdl_vertex_color(255, 255, 255, 128),
{t3, 0.f}},
{{w, 0.f}, sdl_vertex_color(255, 255, 255, 128),
{1.0f, t3}},
{{w, h}, sdl_vertex_color(255, 255, 255, 128),
{1.0f - t3, 1.0f}},
{{0.f, h}, sdl_vertex_color(255, 255, 255, 128),
{0.f, 1.0f - t3}},
};
SDL_SetTextureBlendMode(overlay, SDL_BLENDMODE_BLEND);
render_quad(overlay, verts);
@@ -527,7 +533,7 @@ static void render_scene8(int elapsed) {
if (imgs[0]) {
SDL_SetTextureBlendMode(imgs[0], SDL_BLENDMODE_BLEND);
SDL_SetTextureAlphaMod(imgs[0], (Uint8)SDL_min(alpha, 255));
SDL_RenderCopy(gfx_renderer, imgs[0], NULL, NULL);
sdl_render_texture(gfx_renderer, imgs[0], NULL, NULL);
}
} else if (elapsed < 6 * seg) {
// Segments 26: crossfade image[idx] → image[idx+1]
@@ -536,19 +542,19 @@ static void render_scene8(int elapsed) {
if (imgs[idx]) {
SDL_SetTextureBlendMode(imgs[idx], SDL_BLENDMODE_NONE);
SDL_SetTextureAlphaMod(imgs[idx], 255);
SDL_RenderCopy(gfx_renderer, imgs[idx], NULL, NULL);
sdl_render_texture(gfx_renderer, imgs[idx], NULL, NULL);
}
if (imgs[idx + 1]) {
SDL_SetTextureBlendMode(imgs[idx + 1], SDL_BLENDMODE_BLEND);
SDL_SetTextureAlphaMod(imgs[idx + 1], (Uint8)SDL_min(alpha, 255));
SDL_RenderCopy(gfx_renderer, imgs[idx + 1], NULL, NULL);
sdl_render_texture(gfx_renderer, imgs[idx + 1], NULL, NULL);
}
} else {
// Segment 7: hold image[5] at full opacity
if (imgs[5]) {
SDL_SetTextureBlendMode(imgs[5], SDL_BLENDMODE_NONE);
SDL_SetTextureAlphaMod(imgs[5], 255);
SDL_RenderCopy(gfx_renderer, imgs[5], NULL, NULL);
sdl_render_texture(gfx_renderer, imgs[5], NULL, NULL);
}
}
}
@@ -588,7 +594,7 @@ static void render_scene10(int elapsed) {
SDL_SetTextureBlendMode(tex, SDL_BLENDMODE_NONE);
SDL_SetTextureAlphaMod(tex, 255);
SDL_SetTextureColorMod(tex, 255, 255, 255);
SDL_RenderCopy(gfx_renderer, tex, NULL, NULL);
sdl_render_texture(gfx_renderer, tex, NULL, NULL);
}
} else if (elapsed < 9610) {
// Phase 2: zoom in
@@ -607,7 +613,7 @@ static void render_scene10(int elapsed) {
SDL_SetTextureBlendMode(surface3, SDL_BLENDMODE_BLEND);
SDL_SetTextureAlphaMod(surface3, 255);
SDL_SetTextureColorMod(surface3, 255, 255, 255);
SDL_RenderCopy(gfx_renderer, surface3, &src_rect, NULL);
sdl_render_texture(gfx_renderer, surface3, &src_rect, NULL);
}
} else {
// Phase 3: fade out to black
@@ -618,7 +624,7 @@ static void render_scene10(int elapsed) {
combined < 255 ? SDL_BLENDMODE_BLEND : SDL_BLENDMODE_NONE);
SDL_SetTextureAlphaMod(surface3, (Uint8)combined);
SDL_SetTextureColorMod(surface3, 255, 255, 255);
SDL_RenderCopy(gfx_renderer, surface3, NULL, NULL);
sdl_render_texture(gfx_renderer, surface3, NULL, NULL);
}
}
}
@@ -644,12 +650,12 @@ static void render_scene11(int elapsed) {
// srcY = (bg_h - view_h) * (duration - elapsed) / duration
if (bg) {
int bg_w, bg_h;
SDL_QueryTexture(bg, NULL, NULL, &bg_w, &bg_h);
sdl_get_texture_size(bg, &bg_w, &bg_h);
int scroll = (int)((long long)(bg_h - view_h) * elapsed / duration);
int src_y = SDL_max(0, (bg_h - view_h) - scroll);
SDL_Rect src = {0, src_y, SDL_min(view_w, bg_w), view_h};
SDL_SetTextureBlendMode(bg, SDL_BLENDMODE_NONE);
SDL_RenderCopy(gfx_renderer, bg, &src, NULL);
sdl_render_texture(gfx_renderer, bg, &src, NULL);
}
if (elapsed < unit) {
@@ -659,12 +665,12 @@ static void render_scene11(int elapsed) {
SDL_Texture *tex = slide[2];
if (tex) {
int tw, th;
SDL_QueryTexture(tex, NULL, NULL, &tw, &th);
sdl_get_texture_size(tex, &tw, &th);
int y = (int)((long long)(th + view_h) * (elapsed - unit) / unit) - th;
SDL_SetTextureBlendMode(tex, SDL_BLENDMODE_BLEND);
SDL_SetTextureAlphaMod(tex, 0x80);
SDL_Rect dst = {(view_w - tw) / 2, y, tw, th};
SDL_RenderCopy(gfx_renderer, tex, NULL, &dst);
sdl_render_texture(gfx_renderer, tex, NULL, &dst);
}
} else if (elapsed < unit * 3) {
// Segment 2: overlay[0] (ALK 69) pingpong fade
@@ -675,19 +681,19 @@ static void render_scene11(int elapsed) {
int alpha = (t2 * 255) / (unit / 2);
SDL_SetTextureBlendMode(tex, SDL_BLENDMODE_BLEND);
SDL_SetTextureAlphaMod(tex, (Uint8)SDL_min(alpha, 255));
SDL_RenderCopy(gfx_renderer, tex, NULL, NULL);
sdl_render_texture(gfx_renderer, tex, NULL, NULL);
}
} else if (elapsed < unit * 4) {
// Segment 3: slide[1] (ALK 62) passes top-to-bottom at 50% alpha
SDL_Texture *tex = slide[1];
if (tex) {
int tw, th;
SDL_QueryTexture(tex, NULL, NULL, &tw, &th);
sdl_get_texture_size(tex, &tw, &th);
int y = (int)((long long)(th + view_h) * (elapsed - unit * 3) / unit) - th;
SDL_SetTextureBlendMode(tex, SDL_BLENDMODE_BLEND);
SDL_SetTextureAlphaMod(tex, 0x80);
SDL_Rect dst = {(view_w - tw) / 2, y, tw, th};
SDL_RenderCopy(gfx_renderer, tex, NULL, &dst);
sdl_render_texture(gfx_renderer, tex, NULL, &dst);
}
} else if (elapsed < unit * 5) {
// Segment 4: overlay[1] (ALK 70) pingpong fade
@@ -698,19 +704,19 @@ static void render_scene11(int elapsed) {
int alpha = (t2 * 255) / (unit / 2);
SDL_SetTextureBlendMode(tex, SDL_BLENDMODE_BLEND);
SDL_SetTextureAlphaMod(tex, (Uint8)SDL_min(alpha, 255));
SDL_RenderCopy(gfx_renderer, tex, NULL, NULL);
sdl_render_texture(gfx_renderer, tex, NULL, NULL);
}
} else if (elapsed < unit * 6) {
// Segment 5: slide[0] (ALK 61) passes top-to-bottom at 50% alpha
SDL_Texture *tex = slide[0];
if (tex) {
int tw, th;
SDL_QueryTexture(tex, NULL, NULL, &tw, &th);
sdl_get_texture_size(tex, &tw, &th);
int y = (int)((long long)(th + view_h) * (elapsed - unit * 5) / unit) - th;
SDL_SetTextureBlendMode(tex, SDL_BLENDMODE_BLEND);
SDL_SetTextureAlphaMod(tex, 0x80);
SDL_Rect dst = {(view_w - tw) / 2, y, tw, th};
SDL_RenderCopy(gfx_renderer, tex, NULL, &dst);
sdl_render_texture(gfx_renderer, tex, NULL, &dst);
}
} else if (elapsed < unit * 7) {
// Segment 6: overlay[2] (ALK 71) pingpong fade
@@ -721,7 +727,7 @@ static void render_scene11(int elapsed) {
int alpha = (t2 * 255) / (unit / 2);
SDL_SetTextureBlendMode(tex, SDL_BLENDMODE_BLEND);
SDL_SetTextureAlphaMod(tex, (Uint8)SDL_min(alpha, 255));
SDL_RenderCopy(gfx_renderer, tex, NULL, NULL);
sdl_render_texture(gfx_renderer, tex, NULL, NULL);
}
} else {
// Segment 7: white fade out
@@ -799,7 +805,7 @@ static void render_scene12_3d(int elapsed) {
// NDC → screen
verts[i].position.x = (wx * PROJ_SCALE_X / w + 1.0f) * half_w;
verts[i].position.y = (wy * PROJ_SCALE_Y / w + 1.0f) * half_h;
verts[i].color = (SDL_Color){255, 255, 255, 255};
verts[i].color = sdl_vertex_color(255, 255, 255, 255);
verts[i].tex_coord.x = muv[i][0];
verts[i].tex_coord.y = muv[i][1];
}
@@ -817,7 +823,7 @@ static void render_scene12(int elapsed) {
// 02076ms: bg_a with white fade-in overlay
if (bg_a) {
SDL_SetTextureBlendMode(bg_a, SDL_BLENDMODE_NONE);
SDL_RenderCopy(gfx_renderer, bg_a, NULL, NULL);
sdl_render_texture(gfx_renderer, bg_a, NULL, NULL);
}
int white_alpha = 0xff - (elapsed * 0xff) / 2076;
fade_overlay(0xff, 0xff, 0xff, (Uint8)white_alpha);
@@ -825,32 +831,32 @@ static void render_scene12(int elapsed) {
// 20762226ms: bg_a static
if (bg_a) {
SDL_SetTextureBlendMode(bg_a, SDL_BLENDMODE_NONE);
SDL_RenderCopy(gfx_renderer, bg_a, NULL, NULL);
sdl_render_texture(gfx_renderer, bg_a, NULL, NULL);
}
} else if (elapsed < 3826) {
// 22263826ms: crossfade bg_a → bg_b
if (bg_a) {
SDL_SetTextureBlendMode(bg_a, SDL_BLENDMODE_NONE);
SDL_RenderCopy(gfx_renderer, bg_a, NULL, NULL);
sdl_render_texture(gfx_renderer, bg_a, NULL, NULL);
}
if (bg_b) {
int alpha = ((elapsed - 2226) * 0xff) / 1600;
SDL_SetTextureBlendMode(bg_b, SDL_BLENDMODE_BLEND);
SDL_SetTextureAlphaMod(bg_b, (Uint8)SDL_min(alpha, 0xff));
SDL_RenderCopy(gfx_renderer, bg_b, NULL, NULL);
sdl_render_texture(gfx_renderer, bg_b, NULL, NULL);
}
} else if (elapsed < 3976) {
// 38263976ms: bg_b static
if (bg_b) {
SDL_SetTextureBlendMode(bg_b, SDL_BLENDMODE_NONE);
SDL_RenderCopy(gfx_renderer, bg_b, NULL, NULL);
sdl_render_texture(gfx_renderer, bg_b, NULL, NULL);
}
} else if (elapsed < 7803) {
// 39767803ms: TODO wave distortion on bg_b; for now just show bg_b,
// then fade to white
if (bg_b) {
SDL_SetTextureBlendMode(bg_b, SDL_BLENDMODE_BLEND);
SDL_RenderCopy(gfx_renderer, bg_b, NULL, NULL);
sdl_render_texture(gfx_renderer, bg_b, NULL, NULL);
}
int white_alpha = ((elapsed - 3976) * 0xff) / 3827;
fade_overlay(0xff, 0xff, 0xff, (Uint8)SDL_min(white_alpha, 0xff));
@@ -858,19 +864,19 @@ static void render_scene12(int elapsed) {
// 78039397ms: white bg + 3D rotating quad + overlay fade-in
SDL_SetRenderDrawBlendMode(gfx_renderer, SDL_BLENDMODE_NONE);
SDL_SetRenderDrawColor(gfx_renderer, 0xff, 0xff, 0xff, 0xff);
SDL_RenderFillRect(gfx_renderer, NULL);
sdl_render_fill_rect(gfx_renderer, NULL);
render_scene12_3d(elapsed);
if (overlay) {
int alpha = ((elapsed - 7803) * 0xff) / 1594;
SDL_SetTextureBlendMode(overlay, SDL_BLENDMODE_BLEND);
SDL_SetTextureAlphaMod(overlay, (Uint8)SDL_min(alpha, 0xff));
SDL_RenderCopy(gfx_renderer, overlay, NULL, NULL);
sdl_render_texture(gfx_renderer, overlay, NULL, NULL);
}
} else {
// 939710338ms: overlay static
if (overlay) {
SDL_SetTextureBlendMode(overlay, SDL_BLENDMODE_NONE);
SDL_RenderCopy(gfx_renderer, overlay, NULL, NULL);
sdl_render_texture(gfx_renderer, overlay, NULL, NULL);
}
}
}
+1 -1
View File
@@ -13,7 +13,7 @@ static void Init() {
int p1 = getCaliValue();
int p2 = getCaliValue();
int p3 = getCaliValue();
int *var = getCaliVariable();
vmvar_t *var = getCaliVariable();
*var = 1;
+1 -1
View File
@@ -13,7 +13,7 @@ static void Init() {
int p1 = getCaliValue();
int p2 = getCaliValue();
int p3 = getCaliValue();
int *var = getCaliVariable();
vmvar_t *var = getCaliVariable();
*var = 1;
+9 -6
View File
@@ -31,8 +31,11 @@
#include "ags.h"
#include "font.h"
static FontType ftype;
static int fsize; // フォントの大きさ
static FontSpec dt_spec = {
.type = FONT_GOTHIC,
.weight = FONT_WEIGHT_BOLD,
.size = 12
};
/**
*
@@ -48,8 +51,8 @@ void dt_setfont(FontType type, int size) {
}
#endif
ftype = type;
fsize = size;
dt_spec.type = type;
dt_spec.size = size;
}
/**
@@ -66,12 +69,12 @@ void dt_setfont(FontType type, int size) {
* @return:
*/
int dt_drawtext_col(SDL_Surface *sf, int x, int y, char *buf, int r, int g, int b) {
SDL_Surface *glyph = ags_drawStringToSurface(buf, r, g, b, (FontSpec){ .type = ftype, .size = fsize });
SDL_Surface *glyph = ags_drawStringToSurface(buf, r, g, b, dt_spec);
if (glyph == NULL) return 0;
SDL_Rect rect = {x, y, glyph->w, glyph->h};
SDL_SetSurfaceBlendMode(glyph, SDL_BLENDMODE_NONE);
SDL_BlitSurface(glyph, NULL, sf, &rect);
SDL_FreeSurface(glyph);
sdl_destroy_surface(glyph);
return rect.w;
}
+1 -2
View File
@@ -13,7 +13,7 @@ static void gre_BlendUseAMap(surface_t *dst, int dx, int dy, surface_t *src, int
uint8_t *dp = GETOFFSET_PIXEL(dst, dx, dy);
uint8_t *ap = GETOFFSET_ALPHA(src, sx, sy);
switch (dst->sdl_surface->format->BitsPerPixel) {
switch (sdl_surface_bits_per_pixel(dst->sdl_surface)) {
case 16:
for (int y = 0; y < height; y++) {
uint16_t *yls = (uint16_t *)(sp + y * src->sdl_surface->pitch);
@@ -55,4 +55,3 @@ void gr_blend_alpha_map(surface_t *dst, int dx, int dy, surface_t *src, int sx,
gre_BlendUseAMap(dst, dx, dy, src, sx, sy, sw, sh);
}
+32 -22
View File
@@ -27,7 +27,7 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <SDL.h>
#include "sdl_compat.h"
#include "portab.h"
#include "nact.h"
@@ -103,7 +103,7 @@ void scg_deref(cginfo_t *cg) {
return;
if (cg->sf)
SDL_FreeSurface(cg->sf);
sdl_destroy_surface(cg->sf);
free(cg);
}
@@ -111,8 +111,9 @@ void scg_deref(cginfo_t *cg) {
void scg_create(int wNumCG, int wWidth, int wHeight, int wR, int wG, int wB, int wBlendRate) {
SPCG_ASSERT_NO(wNumCG);
SDL_Surface *sf = SDL_CreateRGBSurfaceWithFormat(0, wWidth, wHeight, 32, SDL_PIXELFORMAT_ARGB8888);
SDL_FillRect(sf, NULL, SDL_MapRGBA(sf->format, wR, wG, wB, wBlendRate));
SDL_Surface *sf = sdl_create_surface(
wWidth, wHeight, 32, SDL_PIXELFORMAT_ARGB8888);
SDL_FillRect(sf, NULL, sdl_map_rgba(sf, wR, wG, wB, wBlendRate));
scg_new(CG_SET, wNumCG, sf);
}
@@ -125,7 +126,7 @@ void scg_create_reverse(int wNumCG, int wNumSrcCG, int wReverseX, int wReverseY)
if (!src)
return;
SDL_Surface *sf = SDL_ConvertSurface(src->sf, src->sf->format, 0);
SDL_Surface *sf = sdl_convert_surface(src->sf, sdl_surface_format(src->sf));
if (wReverseX)
gfx_FlipSurfaceHorizontal(sf);
if (wReverseY)
@@ -142,15 +143,17 @@ void scg_create_stretch(int wNumCG, int wWidth, int wHeight, int wNumSrcCG) {
if (!src)
return;
SDL_Surface *sf = SDL_CreateRGBSurfaceWithFormat(0, wWidth, wHeight, src->sf->format->BitsPerPixel, src->sf->format->format);
SDL_Surface *sf = sdl_create_surface(wWidth, wHeight,
sdl_surface_bits_per_pixel(src->sf), sdl_surface_format(src->sf));
SDL_BlitScaled(src->sf, NULL, sf, NULL);
scg_new(CG_STRETCH, wNumCG, sf);
}
static SDL_Surface *blend(SDL_Surface *base, int x, int y, SDL_Surface *blend, int mode) {
assert(blend->format->format == SDL_PIXELFORMAT_ARGB8888 || blend->format->format == SDL_PIXELFORMAT_XRGB8888);
bool src_has_alpha = SDL_ISPIXELFORMAT_ALPHA(blend->format->format);
SDL_Surface *dst = SDL_ConvertSurfaceFormat(base, SDL_PIXELFORMAT_ARGB8888, 0);
assert(sdl_surface_format(blend) == SDL_PIXELFORMAT_ARGB8888 ||
sdl_surface_format(blend) == SDL_PIXELFORMAT_XRGB8888);
bool src_has_alpha = SDL_ISPIXELFORMAT_ALPHA(sdl_surface_format(blend));
SDL_Surface *dst = sdl_convert_surface(base, SDL_PIXELFORMAT_ARGB8888);
SDL_Rect rect = {x, y, blend->w, blend->h};
SDL_IntersectRect(&(SDL_Rect){0, 0, dst->w, dst->h}, &rect, &rect);
@@ -220,12 +223,14 @@ void scg_create_text(int wNumCG, int wSize, int wR, int wG, int wB, int wText) {
// 勝手に出ていいのかな?
if (svar_length(wText) == 0) return;
SDL_Surface *glyph = ags_drawStringToSurface(svar_get(wText), wR, wG, wB, (FontSpec){ .size = wSize });
FontSpec spec = { .type = FONT_GOTHIC, .weight = FONT_WEIGHT_BOLD, .size = wSize };
SDL_Surface *glyph = ags_drawStringToSurface(svar_get(wText), wR, wG, wB, spec);
SDL_Surface *sf = SDL_CreateRGBSurfaceWithFormat(0, glyph->w, wSize, 32, SDL_PIXELFORMAT_ARGB8888);
SDL_Surface *sf = sdl_create_surface(
glyph->w, wSize, 32, SDL_PIXELFORMAT_ARGB8888);
SDL_SetSurfaceBlendMode(glyph, SDL_BLENDMODE_NONE);
SDL_BlitSurface(glyph, NULL, sf, NULL);
SDL_FreeSurface(glyph);
sdl_destroy_surface(glyph);
scg_new(CG_SET, wNumCG, sf);
}
@@ -244,12 +249,14 @@ void scg_create_textnum(int wNumCG, int wSize, int wR, int wG, int wB, int wFigs
}
sprintf(s, ss, wValue);
SDL_Surface *glyph = ags_drawStringToSurface(s, wR, wG, wB, (FontSpec){ .size = wSize });
FontSpec spec = { .type = FONT_GOTHIC, .weight = FONT_WEIGHT_BOLD, .size = wSize };
SDL_Surface *glyph = ags_drawStringToSurface(s, wR, wG, wB, spec);
SDL_Surface *sf = SDL_CreateRGBSurfaceWithFormat(0, glyph->w, wSize, 32, SDL_PIXELFORMAT_ARGB8888);
SDL_Surface *sf = sdl_create_surface(
glyph->w, wSize, 32, SDL_PIXELFORMAT_ARGB8888);
SDL_SetSurfaceBlendMode(glyph, SDL_BLENDMODE_NONE);
SDL_BlitSurface(glyph, NULL, sf, NULL);
SDL_FreeSurface(glyph);
sdl_destroy_surface(glyph);
scg_new(CG_SET, wNumCG, sf);
}
@@ -263,7 +270,8 @@ void scg_copy(int wNumDstCG, int wNumSrcCG) {
if (!src)
return;
scg_new(CG_SET, wNumDstCG, SDL_ConvertSurface(src->sf, src->sf->format, 0));
scg_new(CG_SET, wNumDstCG,
sdl_convert_surface(src->sf, sdl_surface_format(src->sf)));
}
// CGの一部を切りぬいたCGを作成
@@ -275,7 +283,8 @@ void scg_cut(int wNumDstCG, int wNumSrcCG, int wX, int wY, int wWidth, int wHeig
if (!src)
return;
SDL_Surface *sf = SDL_CreateRGBSurfaceWithFormat(0, wWidth, wHeight, src->sf->format->BitsPerPixel, src->sf->format->format);
SDL_Surface *sf = sdl_create_surface(wWidth, wHeight,
sdl_surface_bits_per_pixel(src->sf), sdl_surface_format(src->sf));
SDL_BlitSurface(src->sf, &(SDL_Rect){wX, wY, wWidth, wHeight}, sf, NULL);
scg_new(CG_SET, wNumDstCG, sf);
@@ -290,8 +299,9 @@ void scg_partcopy(int wNumDstCG, int wNumSrcCG, int wX, int wY, int wWidth, int
if (!src)
return;
SDL_Surface *sf = SDL_CreateRGBSurfaceWithFormat(0, src->sf->w, src->sf->h, src->sf->format->BitsPerPixel, src->sf->format->format);
SDL_FillRect(sf, NULL, SDL_MapRGBA(sf->format, 0, 0, 0, 255));
SDL_Surface *sf = sdl_create_surface(src->sf->w, src->sf->h,
sdl_surface_bits_per_pixel(src->sf), sdl_surface_format(src->sf));
SDL_FillRect(sf, NULL, sdl_map_rgba(sf, 0, 0, 0, 255));
SDL_BlitSurface(src->sf, &(SDL_Rect){wX, wY, wWidth, wHeight}, sf, &(SDL_Rect){wX, wY, wWidth, wHeight});
scg_new(CG_SET, wNumDstCG, sf);
@@ -331,7 +341,7 @@ int scg_querytype(int wNumCG) {
}
// CGの大きさを取得
bool scg_querysize(int wNumCG, int *w, int *h) {
bool scg_querysize(int wNumCG, vmvar_t *w, vmvar_t *h) {
if (wNumCG >= (CGMAX -1)) goto errexit;
if (!cg_store || !cg_store[wNumCG]) goto errexit;
if (cg_store[wNumCG]->sf == NULL) goto errexit;
@@ -351,7 +361,7 @@ int scg_querybpp(int wNumCG) {
if (wNumCG >= (CGMAX -1)) return 0;
if (!cg_store || !cg_store[wNumCG]) return 0;
if (cg_store[wNumCG]->sf == NULL) return 0;
return cg_store[wNumCG]->sf->format->BitsPerPixel;
return sdl_surface_bits_per_pixel(cg_store[wNumCG]->sf);
}
// CGの alphamap が存在するかを取得
@@ -359,5 +369,5 @@ bool scg_existalphamap(int wNumCG) {
if (wNumCG >= (CGMAX -1)) return false;
if (!cg_store || !cg_store[wNumCG]) return false;
if (cg_store[wNumCG]->sf == NULL) return false;
return SDL_ISPIXELFORMAT_ALPHA(cg_store[wNumCG]->sf->format->format);
return SDL_ISPIXELFORMAT_ALPHA(sdl_surface_format(cg_store[wNumCG]->sf));
}

Some files were not shown because too many files have changed in this diff Show More