Merge branch 'master' into translations

This commit is contained in:
Vicki Pfau 2025-05-02 04:15:33 -07:00
commit 097eb338bc
103 changed files with 4087 additions and 1131 deletions

24
CHANGES
View File

@ -5,10 +5,12 @@ Features:
- New option to lock the maximum frame size
- Memory access and information logging
- 3DS: Add faster "loose" sync mode, default enabled
- Vita: Allow using rear touch pads as L2/L3/R2/R3
- Scripting: New `input` API for getting raw keyboard/mouse/controller state
- Scripting: New `storage` API for saving data for a script, e.g. settings
- Scripting: New `image` and `canvas` APIs for drawing images and displaying on-screen
- Scripting: Debugger integration to allow for breakpoints and watchpoints
- Scripting: Add support for running scripts at startup
- New unlicensed GB mappers: NT (older types 1 and 2), Li Cheng, GGB-81, Sintax
- Initial support for bootleg GBA multicarts
- Debugger: Add range watchpoints
@ -23,23 +25,22 @@ Emulation fixes:
- GBA Video: Disable BG target 1 blending when OBJ blending (fixes mgba.io/i/2722)
- GBA Video: Improve emulation of window start/end conditions (fixes mgba.io/i/1945)
Other fixes:
- ARM Debugger: Fix disassembly of ror r0 barrel shift (fixes mgba.io/i/3412)
- Core: Fix inconsistencies with setting game-specific overrides (fixes mgba.io/i/2963)
- Debugger: Fix writing to specific segment in command-line debugger
- FFmpeg: Fix failing to record videos with CRF video (fixes mgba.io/i/3368)
- GB Core: Fix cloning savedata when backing file is outdated (fixes mgba.io/i/3388)
- GBA: Fix getting game info for multiboot ROMs
- GBA Core: Fix booting into BIOS when skip BIOS is enabled
- GBA Hardware: Fix loading states unconditionally overwriting GPIO memory
- mGUI: Load parent directory if last used directory is missing (fixes mgba.io/i/3379)
- Qt: Fix savestate preview sizes with different scales (fixes mgba.io/i/2560)
- Qt: Fix potential crash when configuring shortcuts
- Qt: Fix regression where loading BIOS creates a save file (fixes mgba.io/i/3359)
- Qt: Fix selecting high tiles in tile and map views (fixes mgba.io/i/3461)
Misc:
- 3DS: Change title ID to avoid conflict with commercial title (fixes mgba.io/i/3023)
- Core: Handle relative paths for saves, screenshots, etc consistently (fixes mgba.io/i/2826)
- Core: Improve rumble emulation by averaging state over entire frame (fixes mgba.io/i/3232)
- Core: Add MD5 hashing for ROMs
- Core: Add support for specifying an arbitrary portable directory
- Core: Add SHA1 hashing for ROMs
- FFmpeg: Add Ut Video option
- GB: Prevent incompatible BIOSes from being used on differing models
- GB Serialize: Add missing savestate support for MBC6 and NT (newer)
- GBA: Improve detection of valid ELF ROMs
@ -61,11 +62,24 @@ Misc:
- Qt: Support building against Qt 6
- Qt: Add shortcuts to increment fast forward speed (mgba.io/i/2903)
- Qt: Enable ROM preloading by default
- Qt: Throttle fatal error dialogs
- Res: Port hq2x and OmniScale shaders from SameBoy
- Res: Port NSO-gba-colors shader (closes mgba.io/i/2834)
- Res: Update gba-colors shader (closes mgba.io/i/2976)
- Scripting: Add `callbacks:oneshot` for single-call callbacks
0.10.5: (2025-03-08)
Other fixes:
- ARM Debugger: Fix disassembly of ror r0 barrel shift (fixes mgba.io/i/3412)
- FFmpeg: Fix failing to record videos with CRF video (fixes mgba.io/i/3368)
- GB Core: Fix cloning savedata when backing file is outdated (fixes mgba.io/i/3388)
- GBA Cheats: Let VBA-style codes patch ROM (fixes mgba.io/i/3423)
- GBA Core: Fix booting into BIOS when skip BIOS is enabled
- GBA Hardware: Fix loading states unconditionally overwriting GPIO memory
- Updater: Fix rewriting folders and files on Windows (fixes mgba.io/i/3384)
- Wii: Fix crash on loading large ZIP files (fixes mgba.io/i/3404)
Misc:
- GB: Allow use of CGB-E and AGB-0 BIOS versions (closes mgba.io/i/3427)
0.10.4: (2024-12-07)
Emulation fixes:

View File

@ -1,4 +1,4 @@
cmake_minimum_required(VERSION 3.3)
cmake_minimum_required(VERSION 3.10)
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/src/platform/cmake/")
if(POLICY CMP0025)
@ -857,7 +857,7 @@ if(ENABLE_SCRIPTING)
endif()
if(ENABLE_VFS)
list(APPEND ENABLES VFS)
list(APPEND ENABLES VFS DIRECTORIES)
endif()
foreach(FEATURE IN LISTS FEATURES)
@ -1001,7 +1001,7 @@ endif()
if(BUILD_LIBRETRO)
file(GLOB RETRO_SRC ${CMAKE_CURRENT_SOURCE_DIR}/src/platform/libretro/*.c)
add_library(${BINARY_NAME}_libretro SHARED ${CORE_SRC} ${RETRO_SRC} ${VFS_SRC})
add_library(${BINARY_NAME}_libretro SHARED ${CORE_SRC} ${RETRO_SRC} ${CORE_VFS_SRC})
add_dependencies(${BINARY_NAME}_libretro ${BINARY_NAME}-version-info)
set_target_properties(${BINARY_NAME}_libretro PROPERTIES PREFIX "" COMPILE_DEFINITIONS "__LIBRETRO__;COLOR_16_BIT;COLOR_5_6_5;DISABLE_THREADING;MGBA_STANDALONE;${OS_DEFINES};${FUNCTION_DEFINES};ENABLE_VFS;MINIMAL_CORE=2")
target_link_libraries(${BINARY_NAME}_libretro ${OS_LIB})

33
include/mgba-util/sha1.h Normal file
View File

@ -0,0 +1,33 @@
/* Copyright (c) 2013-2025 Jeffrey Pfau
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*
* Based on https://github.com/clibs/sha1
*/
#ifndef SHA1_H
#define SHA1_H
#include <mgba-util/common.h>
CXX_GUARD_START
struct SHA1Context {
uint32_t state[5];
uint32_t count[2];
unsigned char buffer[64];
};
void sha1Init(struct SHA1Context* ctx);
void sha1Update(struct SHA1Context* ctx, const void* input, size_t len);
void sha1Finalize(uint8_t digest[20], struct SHA1Context* ctx);
void sha1Buffer(const void* input, size_t len, uint8_t* result);
struct VFile;
bool sha1File(struct VFile* vf, uint8_t* result);
CXX_GUARD_END
#endif

View File

@ -50,7 +50,7 @@ struct VFile {
bool (*sync)(struct VFile* vf, void* buffer, size_t size);
};
#ifdef ENABLE_VFS
#if defined(ENABLE_VFS) && defined(ENABLE_DIRECTORIES)
struct VDirEntry {
const char* (*name)(struct VDirEntry* vde);
enum VFSType (*type)(struct VDirEntry* vde);
@ -64,7 +64,9 @@ struct VDir {
struct VDir* (*openDir)(struct VDir* vd, const char* name);
bool (*deleteFile)(struct VDir* vd, const char* name);
};
#endif
#ifdef ENABLE_VFS
struct VFile* VFileOpen(const char* path, int flags);
#endif
@ -85,7 +87,7 @@ struct VFile* VFileMemChunk(const void* mem, size_t size);
struct mCircleBuffer;
struct VFile* VFileFIFO(struct mCircleBuffer* backing);
#ifdef ENABLE_VFS
#if defined(ENABLE_VFS) && defined(ENABLE_DIRECTORIES)
struct VDir* VDirOpen(const char* path);
struct VDir* VDirOpenArchive(const char* path);

View File

@ -118,7 +118,7 @@ bool mCheatSaveFile(struct mCheatDevice*, struct VFile*);
bool mCheatParseLibretroFile(struct mCheatDevice*, struct VFile*);
bool mCheatParseEZFChtFile(struct mCheatDevice*, struct VFile*);
#ifdef ENABLE_VFS
#if defined(ENABLE_VFS) && defined(ENABLE_DIRECTORIES)
void mCheatAutosave(struct mCheatDevice*);
#endif

View File

@ -28,11 +28,6 @@ enum mPlatform {
mPLATFORM_GB = 1,
};
enum mCoreChecksumType {
mCHECKSUM_CRC32,
mCHECKSUM_MD5,
};
struct mAudioBuffer;
struct mCoreConfig;
struct mCoreSync;
@ -47,7 +42,7 @@ struct mCore {
struct mDebuggerSymbols* symbolTable;
struct mVideoLogger* videoLogger;
#ifdef ENABLE_VFS
#if defined(ENABLE_VFS) && defined(ENABLE_DIRECTORIES)
struct mDirectorySet dirs;
#endif
#ifndef MINIMAL_CORE
@ -188,18 +183,20 @@ bool mCorePreloadFile(struct mCore* core, const char* path);
bool mCorePreloadVFCB(struct mCore* core, struct VFile* vf, void (cb)(size_t, size_t, void*), void* context);
bool mCorePreloadFileCB(struct mCore* core, const char* path, void (cb)(size_t, size_t, void*), void* context);
bool mCoreLoadSaveFile(struct mCore* core, const char* path, bool temporary);
#if defined(ENABLE_VFS) && defined(ENABLE_DIRECTORIES)
bool mCoreAutoloadSave(struct mCore* core);
bool mCoreAutoloadPatch(struct mCore* core);
bool mCoreAutoloadCheats(struct mCore* core);
bool mCoreLoadSaveFile(struct mCore* core, const char* path, bool temporary);
bool mCoreSaveState(struct mCore* core, int slot, int flags);
bool mCoreLoadState(struct mCore* core, int slot, int flags);
struct VFile* mCoreGetState(struct mCore* core, int slot, bool write);
void mCoreDeleteState(struct mCore* core, int slot);
void mCoreTakeScreenshot(struct mCore* core);
#endif
bool mCoreTakeScreenshotVF(struct mCore* core, struct VFile* vf);
#endif

View File

@ -10,7 +10,7 @@
CXX_GUARD_START
#ifdef ENABLE_VFS
#if defined(ENABLE_VFS) && defined(ENABLE_DIRECTORIES)
struct VDir;
struct mDirectorySet {

View File

@ -188,6 +188,12 @@ struct mCoreRegisterInfo {
enum mCoreRegisterType type;
};
enum mCoreChecksumType {
mCHECKSUM_CRC32,
mCHECKSUM_MD5,
mCHECKSUM_SHA1,
};
CXX_GUARD_END
#endif

View File

@ -15,6 +15,8 @@ CXX_GUARD_START
#include <mgba/core/core.h>
#include <mgba-util/vector.h>
#define M_LIBRARY_MODEL_UNKNOWN -1
struct mLibraryEntry {
const char* base;
const char* filename;
@ -24,6 +26,9 @@ struct mLibraryEntry {
enum mPlatform platform;
size_t filesize;
uint32_t crc32;
uint8_t md5[16];
uint8_t sha1[20];
int platformModels;
};
#ifdef USE_SQLITE3

View File

@ -49,10 +49,11 @@ DECL_BIT(RTCCommandData, Reading, 7);
struct GBARTC {
int32_t bytesRemaining;
int32_t transferStep;
int32_t bitsRead;
int32_t bits;
int32_t commandActive;
bool sckEdge;
bool sioOutput;
RTCCommandData command;
RTCControl control;
uint8_t time[7];
@ -68,8 +69,9 @@ struct GBACartridgeHardware {
enum GPIODirection readWrite;
uint16_t* gpioBase;
uint16_t pinState;
uint16_t direction;
uint8_t writeLatch;
uint8_t pinState;
uint8_t direction;
struct GBARTC rtc;

View File

@ -165,8 +165,12 @@ mLOG_DECLARE_CATEGORY(GBA_STATE);
* | 0x00288 - 0x0028B: DMA next count
* | 0x0028C - 0x0028F: DMA next event
* 0x00290 - 0x002C3: GPIO state
* | 0x00290 - 0x00291: Pin state
* | 0x00292 - 0x00293: Direction state
* | 0x00290: Pin state
* | 0x00291: Write latch
* | 0x00292: Direction state
* | 0x00293: Flags
* | bit 0: RTC SIO output
* | bit 1 - 7: Reserved
* | 0x00294 - 0x002B6: RTC state (see hardware.h for format)
* | 0x002B7 - 0x002B7: GPIO devices
* | bit 0: Has RTC values
@ -183,7 +187,7 @@ mLOG_DECLARE_CATEGORY(GBA_STATE);
* | bit 0: Is read enabled
* | bit 1: Gyroscope sample is edge
* | bit 2: Light sample is edge
* | bit 3: Reserved
* | bit 3: RTC SCK is edge
* | bits 4 - 15: Light counter
* | 0x002C0 - 0x002C0: Light sample
* | 0x002C1: Flags
@ -282,6 +286,7 @@ DECL_BITFIELD(GBASerializedHWFlags1, uint16_t);
DECL_BIT(GBASerializedHWFlags1, ReadWrite, 0);
DECL_BIT(GBASerializedHWFlags1, GyroEdge, 1);
DECL_BIT(GBASerializedHWFlags1, LightEdge, 2);
DECL_BIT(GBASerializedHWFlags1, RtcSckEdge, 3);
DECL_BITS(GBASerializedHWFlags1, LightCounter, 4, 12);
DECL_BITFIELD(GBASerializedHWFlags2, uint8_t);
@ -289,6 +294,9 @@ DECL_BITS(GBASerializedHWFlags2, TiltState, 0, 2);
DECL_BITS(GBASerializedHWFlags2, GbpInputsPosted, 2, 2);
DECL_BITS(GBASerializedHWFlags2, GbpTxPosition, 4, 4);
DECL_BITFIELD(GBASerializedHWFlags3, uint8_t);
DECL_BITS(GBASerializedHWFlags3, RtcSioOutput, 0, 1);
DECL_BITFIELD(GBASerializedUnlCartFlags, uint16_t);
DECL_BITS(GBASerializedUnlCartFlags, Type, 0, 5);
DECL_BITS(GBASerializedUnlCartFlags, Subtype, 5, 3);
@ -376,16 +384,18 @@ struct GBASerializedState {
} dma[4];
struct {
uint16_t pinState;
uint16_t pinDirection;
uint8_t pinState;
uint8_t writeLatch;
uint8_t pinDirection;
GBASerializedHWFlags3 flags3;
int32_t rtcBytesRemaining;
int32_t rtcTransferStep;
int32_t reserved0;
int32_t rtcBitsRead;
int32_t rtcBits;
int32_t rtcCommandActive;
RTCCommandData rtcCommand;
uint8_t rtcControl;
uint8_t reserved[3];
uint8_t reserved1[3];
uint8_t time[7];
uint8_t devices;
uint16_t gyroSample;

View File

@ -8,6 +8,6 @@ Name=mGBA
GenericName=Game Boy Advance Emulator
Comment=Nintendo Game Boy Advance Emulator
Categories=Game;Emulator;
MimeType=application/x-gameboy-advance-rom;application/x-agb-rom;application/x-gba-rom;
Keywords=emulator;Nintendo;advance;gba;Game Boy Advance;
MimeType=application/x-gameboy-advance-rom;application/x-agb-rom;application/x-gba-rom;application/x-gameboy-rom;application/x-gameboy-color-rom;
Keywords=emulator;Nintendo;advance;gba;gbc;gb;Game Boy Advance;Game Boy Color; Game Boy;
StartupWMClass=mGBA

File diff suppressed because it is too large Load Diff

View File

@ -44,27 +44,25 @@ local state = {
[C.PLATFORM.GB] = 7,
}
}
state.overlay = canvas:newLayer(32, 16)
state.painter = image.newPainter(state.overlay.image)
state.painter:setBlend(false)
state.painter:setFill(true)
function state.update()
local keys = util.expandBitmask(emu:getKeys())
local maxKey = state.maxKey[emu:platform()]
for key = 0, maxKey do
if emu:getKey(key) ~= 0 then
state.painter:setFillColor(0x80FFFFFF)
else
state.painter:setFillColor(0x40404040)
end
state.drawButton[key](state)
function state.create()
if state.overlay ~= nil then
return true
end
state.overlay:update()
if canvas == nil then
return false
end
state.overlay = canvas:newLayer(32, 16)
if state.overlay == nil then
return false
end
state.painter = image.newPainter(state.overlay.image)
state.painter:setBlend(false)
state.painter:setFill(true)
return true
end
function state.reset()
function state.update()
local endX = canvas:screenWidth() - 32
local endY = canvas:screenHeight() - 16
@ -112,11 +110,32 @@ function state.reset()
pos.y = pos.y + input_display.offset.y;
state.overlay:setPosition(pos.x, pos.y);
local keys = util.expandBitmask(emu:getKeys())
local maxKey = state.maxKey[emu:platform()]
for key = 0, maxKey do
if emu:getKey(key) ~= 0 then
state.painter:setFillColor(0x80FFFFFF)
else
state.painter:setFillColor(0x40404040)
end
state.drawButton[key](state)
end
state.overlay:update()
end
function state.reset()
if not state.create() then
return
end
state.painter:setFillColor(0x40808080)
state.painter:drawRectangle(0, 0, 32, 16)
state.overlay:update()
end
input_display.state = state
state.reset()
callbacks:add("frame", state.update)
callbacks:add("start", state.reset)

View File

@ -466,20 +466,24 @@ local gameLeafGreenEnR1 = gameLeafGreenEn:new{
_speciesNameTable=0x245f2c,
}
gameCodes = {
["DMG-AAUE"]=gameGSEn, -- Gold
["DMG-AAXE"]=gameGSEn, -- Silver
["CGB-BYTE"]=gameCrystalEn,
["AGB-AXVE"]=gameRubyEn,
["AGB-AXPE"]=gameSapphireEn,
["AGB-BPEE"]=gameEmeraldEn,
["AGB-BPRE"]=gameFireRedEn,
["AGB-BPGE"]=gameLeafGreenEn,
local gameCodes = {
[C.PLATFORM.GB] = {
["AAUE"] = gameGSEn, -- Gold
["AAXE"] = gameGSEn, -- Silver
["BYTE"] = gameCrystalEn,
},
[C.PLATFORM.GBA] = {
["AXVE"] = gameRubyEn,
["AXPE"] = gameSapphireEn,
["BPEE"] = gameEmeraldEn,
["BPRE"] = gameFireRedEn,
["BPGE"] = gameLeafGreenEn,
}
}
-- These versions have slight differences and/or cannot be uniquely
-- identified by their in-header game codes, so fall back on a CRC32
gameCrc32 = {
local gameCrc32 = {
[0x9f7fdd53] = gameRBEn, -- Red
[0xd6da8a1a] = gameRBEn, -- Blue
[0x7d527d62] = gameYellowEn,
@ -510,7 +514,7 @@ function detectGame()
end
game = gameCrc32[checksum]
if not game then
game = gameCodes[emu:getGameCode()]
game = gameCodes[emu:platform()][emu:getGameCode()]
end
if not game then

View File

@ -743,11 +743,11 @@ DEFINE_INSTRUCTION_ARM(MRSR, \
cpu->gprs[rd] = cpu->spsr.packed;)
DEFINE_INSTRUCTION_ARM(MSRI,
int c = opcode & 0x00010000;
int f = opcode & 0x00080000;
int rotate = (opcode & 0x00000F00) >> 7;
int32_t operand = ROR(opcode & 0x000000FF, rotate);
int32_t mask = (c ? 0x000000FF : 0) | (f ? 0xFF000000 : 0);
uint32_t c = opcode & 0x00010000;
uint32_t f = opcode & 0x00080000;
uint32_t rotate = (opcode & 0x00000F00) >> 7;
uint32_t operand = ROR(opcode & 0x000000FF, rotate);
uint32_t mask = (c ? 0x000000FF : 0) | (f ? 0xFF000000 : 0);
if (mask & PSR_USER_MASK) {
cpu->cpsr.packed = (cpu->cpsr.packed & ~PSR_USER_MASK) | (operand & PSR_USER_MASK);
}
@ -769,11 +769,11 @@ DEFINE_INSTRUCTION_ARM(MSRI,
})
DEFINE_INSTRUCTION_ARM(MSRRI,
int c = opcode & 0x00010000;
int f = opcode & 0x00080000;
int rotate = (opcode & 0x00000F00) >> 7;
int32_t operand = ROR(opcode & 0x000000FF, rotate);
int32_t mask = (c ? 0x000000FF : 0) | (f ? 0xFF000000 : 0);
uint32_t c = opcode & 0x00010000;
uint32_t f = opcode & 0x00080000;
uint32_t rotate = (opcode & 0x00000F00) >> 7;
uint32_t operand = ROR(opcode & 0x000000FF, rotate);
uint32_t mask = (c ? 0x000000FF : 0) | (f ? 0xFF000000 : 0);
mask &= PSR_USER_MASK | PSR_PRIV_MASK | PSR_STATE_MASK;
cpu->spsr.packed = (cpu->spsr.packed & ~mask) | (operand & mask) | 0x00000010;)

View File

@ -622,7 +622,7 @@ bool mCheatSaveFile(struct mCheatDevice* device, struct VFile* vf) {
return true;
}
#ifdef ENABLE_VFS
#if defined(ENABLE_VFS) && defined(ENABLE_DIRECTORIES)
void mCheatAutosave(struct mCheatDevice* device) {
if (!device->autosave) {
return;

View File

@ -96,8 +96,9 @@ struct mCore* mCoreCreate(enum mPlatform platform) {
#endif
struct mCore* mCoreFind(const char* path) {
struct VDir* archive = VDirOpenArchive(path);
struct mCore* core = NULL;
#if defined(ENABLE_VFS) && defined(ENABLE_DIRECTORIES)
struct VDir* archive = VDirOpenArchive(path);
if (archive) {
struct VDirEntry* dirent = archive->listNext(archive);
while (dirent) {
@ -114,7 +115,9 @@ struct mCore* mCoreFind(const char* path) {
dirent = archive->listNext(archive);
}
archive->close(archive);
} else {
} else
#endif
{
struct VFile* vf = VFileOpen(path, O_RDONLY);
if (!vf) {
return NULL;
@ -133,7 +136,15 @@ bool mCoreLoadFile(struct mCore* core, const char* path) {
#ifdef FIXED_ROM_BUFFER
return mCorePreloadFile(core, path);
#else
#if defined(ENABLE_VFS) && defined(ENABLE_DIRECTORIES)
struct VFile* rom = mDirectorySetOpenPath(&core->dirs, path, core->isROM);
#else
struct VFile* rom = VFileOpen(path, O_RDONLY);
if (rom && !core->isROM(rom)) {
rom->close(rom);
rom = NULL;
}
#endif
if (!rom) {
return false;
}
@ -210,7 +221,15 @@ bool mCorePreloadVFCB(struct mCore* core, struct VFile* vf, void (cb)(size_t, si
}
bool mCorePreloadFileCB(struct mCore* core, const char* path, void (cb)(size_t, size_t, void*), void* context) {
#if defined(ENABLE_VFS) && defined(ENABLE_DIRECTORIES)
struct VFile* rom = mDirectorySetOpenPath(&core->dirs, path, core->isROM);
#else
struct VFile* rom = VFileOpen(path, O_RDONLY);
if (rom && !core->isROM(rom)) {
rom->close(rom);
rom = NULL;
}
#endif
if (!rom) {
return false;
}
@ -222,6 +241,19 @@ bool mCorePreloadFileCB(struct mCore* core, const char* path, void (cb)(size_t,
return ret;
}
bool mCoreLoadSaveFile(struct mCore* core, const char* path, bool temporary) {
struct VFile* vf = VFileOpen(path, O_CREAT | O_RDWR);
if (!vf) {
return false;
}
if (temporary) {
return core->loadTemporarySave(core, vf);
} else {
return core->loadSave(core, vf);
}
}
#if defined(ENABLE_VFS) && defined(ENABLE_DIRECTORIES)
bool mCoreAutoloadSave(struct mCore* core) {
if (!core->dirs.save) {
return false;
@ -277,18 +309,6 @@ bool mCoreAutoloadCheats(struct mCore* core) {
return success;
}
bool mCoreLoadSaveFile(struct mCore* core, const char* path, bool temporary) {
struct VFile* vf = VFileOpen(path, O_CREAT | O_RDWR);
if (!vf) {
return false;
}
if (temporary) {
return core->loadTemporarySave(core, vf);
} else {
return core->loadSave(core, vf);
}
}
bool mCoreSaveState(struct mCore* core, int slot, int flags) {
struct VFile* vf = mCoreGetState(core, slot, true);
if (!vf) {
@ -373,6 +393,7 @@ void mCoreTakeScreenshot(struct mCore* core) {
mLOG(STATUS, WARN, "Failed to take screenshot");
}
#endif
#endif
bool mCoreTakeScreenshotVF(struct mCore* core, struct VFile* vf) {
#ifdef USE_PNG
@ -406,7 +427,7 @@ void mCoreLoadConfig(struct mCore* core) {
void mCoreLoadForeignConfig(struct mCore* core, const struct mCoreConfig* config) {
mCoreConfigMap(config, &core->opts);
#ifdef ENABLE_VFS
#if defined(ENABLE_VFS) && defined(ENABLE_DIRECTORIES)
mDirectorySetMapOptions(&core->dirs, &core->opts);
#endif
if (core->opts.audioBuffers) {

View File

@ -8,7 +8,7 @@
#include <mgba/core/config.h>
#include <mgba-util/vfs.h>
#ifdef ENABLE_VFS
#if defined(ENABLE_VFS) && defined(ENABLE_DIRECTORIES)
void mDirectorySetInit(struct mDirectorySet* dirs) {
dirs->base = NULL;
dirs->archive = NULL;

View File

@ -53,6 +53,10 @@
#cmakedefine ENABLE_DEBUGGERS
#endif
#ifndef ENABLE_DIRECTORIES
#cmakedefine ENABLE_DIRECTORIES
#endif
#ifndef ENABLE_GDB_STUB
#cmakedefine ENABLE_GDB_STUB
#endif

View File

@ -9,6 +9,11 @@
#include <mgba-util/string.h>
#include <mgba-util/vfs.h>
#ifdef M_CORE_GB
#include <mgba/gb/interface.h>
#include <mgba/internal/gb/gb.h>
#endif
#ifdef USE_SQLITE3
#include <sqlite3.h>
@ -33,7 +38,10 @@ struct mLibrary {
#define CONSTRAINTS_ROMONLY \
"CASE WHEN :useSize THEN roms.size = :size ELSE 1 END AND " \
"CASE WHEN :usePlatform THEN roms.platform = :platform ELSE 1 END AND " \
"CASE WHEN :useModels THEN roms.models & :models ELSE 1 END AND " \
"CASE WHEN :useCrc32 THEN roms.crc32 = :crc32 ELSE 1 END AND " \
"CASE WHEN :useMd5 THEN roms.md5 = :md5 ELSE 1 END AND " \
"CASE WHEN :useSha1 THEN roms.sha1 = :sha1 ELSE 1 END AND " \
"CASE WHEN :useInternalCode THEN roms.internalCode = :internalCode ELSE 1 END"
#define CONSTRAINTS \
@ -58,6 +66,20 @@ static void _bindConstraints(sqlite3_stmt* statement, const struct mLibraryEntry
sqlite3_bind_int(statement, index, constraints->crc32);
}
if (memcmp(constraints->md5, &(uint8_t[16]) {0}, 16) != 0) {
useIndex = sqlite3_bind_parameter_index(statement, ":useMd5");
index = sqlite3_bind_parameter_index(statement, ":md5");
sqlite3_bind_int(statement, useIndex, 1);
sqlite3_bind_blob(statement, index, constraints->md5, 16, NULL);
}
if (memcmp(constraints->sha1, &(uint8_t[20]) {0}, 20) != 0) {
useIndex = sqlite3_bind_parameter_index(statement, ":useSha1");
index = sqlite3_bind_parameter_index(statement, ":sha1");
sqlite3_bind_int(statement, useIndex, 1);
sqlite3_bind_blob(statement, index, constraints->sha1, 20, NULL);
}
if (constraints->filesize) {
useIndex = sqlite3_bind_parameter_index(statement, ":useSize");
index = sqlite3_bind_parameter_index(statement, ":size");
@ -92,12 +114,40 @@ static void _bindConstraints(sqlite3_stmt* statement, const struct mLibraryEntry
sqlite3_bind_int(statement, useIndex, 1);
sqlite3_bind_int(statement, index, constraints->platform);
}
if (constraints->platformModels != M_LIBRARY_MODEL_UNKNOWN) {
useIndex = sqlite3_bind_parameter_index(statement, ":useModels");
index = sqlite3_bind_parameter_index(statement, ":models");
sqlite3_bind_int(statement, useIndex, 1);
sqlite3_bind_int(statement, index, constraints->platformModels);
}
}
struct mLibrary* mLibraryCreateEmpty(void) {
return mLibraryLoad(":memory:");
}
static int _mLibraryTableVersion(struct mLibrary* library, const char* tableName) {
int version = -1;
static const char getVersion[] = "SELECT version FROM version WHERE tname=?";
sqlite3_stmt* getVersionStmt;
if (sqlite3_prepare_v2(library->db, getVersion, -1, &getVersionStmt, NULL)) {
goto error;
}
sqlite3_clear_bindings(getVersionStmt);
sqlite3_reset(getVersionStmt);
sqlite3_bind_text(getVersionStmt, 1, tableName, -1, SQLITE_TRANSIENT);
if (sqlite3_step(getVersionStmt) != SQLITE_DONE) {
version = sqlite3_column_int(getVersionStmt, 0);
}
error:
sqlite3_finalize(getVersionStmt);
return version;
}
struct mLibrary* mLibraryLoad(const char* path) {
struct mLibrary* library = malloc(sizeof(*library));
memset(library, 0, sizeof(*library));
@ -124,6 +174,7 @@ struct mLibrary* mLibraryLoad(const char* path) {
"\n internalTitle TEXT,"
"\n internalCode TEXT,"
"\n platform INTEGER NOT NULL DEFAULT -1,"
"\n models INTEGER NULL,"
"\n size INTEGER,"
"\n crc32 INTEGER,"
"\n md5 BLOB,"
@ -139,20 +190,39 @@ struct mLibrary* mLibraryLoad(const char* path) {
"\n CONSTRAINT location UNIQUE (path, rootid)"
"\n );"
"\n CREATE INDEX IF NOT EXISTS crc32 ON roms (crc32);"
"\n CREATE INDEX IF NOT EXISTS md5 ON roms (md5);"
"\n CREATE INDEX IF NOT EXISTS sha1 ON roms (sha1);"
"\n INSERT OR IGNORE INTO version (tname, version) VALUES ('version', 1);"
"\n INSERT OR IGNORE INTO version (tname, version) VALUES ('roots', 1);"
"\n INSERT OR IGNORE INTO version (tname, version) VALUES ('roms', 1);"
"\n INSERT OR IGNORE INTO version (tname, version) VALUES ('roms', 2);"
"\n INSERT OR IGNORE INTO version (tname, version) VALUES ('paths', 1);";
if (sqlite3_exec(library->db, createTables, NULL, NULL, NULL)) {
goto error;
}
int romsTableVersion = _mLibraryTableVersion(library, "roms");
if (romsTableVersion < 0) {
goto error;
} else if (romsTableVersion < 2) {
static const char upgradeRomsTable[] =
" ALTER TABLE roms"
"\n ADD COLUMN models INTEGER NULL";
if (sqlite3_exec(library->db, upgradeRomsTable, NULL, NULL, NULL)) {
goto error;
}
static const char updateRomsTableVersion[] = "UPDATE version SET version=2 WHERE tname='roms'";
if (sqlite3_exec(library->db, updateRomsTableVersion, NULL, NULL, NULL)) {
goto error;
}
}
static const char insertPath[] = "INSERT INTO paths (romid, path, customTitle, rootid) VALUES (?, ?, ?, ?);";
if (sqlite3_prepare_v2(library->db, insertPath, -1, &library->insertPath, NULL)) {
goto error;
}
static const char insertRom[] = "INSERT INTO roms (crc32, size, internalCode, platform) VALUES (:crc32, :size, :internalCode, :platform);";
static const char insertRom[] = "INSERT INTO roms (crc32, md5, sha1, size, internalCode, platform, models) VALUES (:crc32, :md5, :sha1, :size, :internalCode, :platform, :models);";
if (sqlite3_prepare_v2(library->db, insertRom, -1, &library->insertRom, NULL)) {
goto error;
}
@ -297,7 +367,18 @@ bool _mLibraryAddEntry(struct mLibrary* library, const char* filename, const cha
snprintf(entry.internalCode, sizeof(entry.internalCode), "%s-%s", info.system, info.code);
strlcpy(entry.internalTitle, info.title, sizeof(entry.internalTitle));
core->checksum(core, &entry.crc32, mCHECKSUM_CRC32);
core->checksum(core, &entry.md5, mCHECKSUM_MD5);
core->checksum(core, &entry.sha1, mCHECKSUM_SHA1);
entry.platform = core->platform(core);
entry.platformModels = M_LIBRARY_MODEL_UNKNOWN;
#ifdef M_CORE_GB
if (entry.platform == mPLATFORM_GB) {
struct GB* gb = (struct GB*) core->board;
if (gb->memory.rom) {
entry.platformModels = GBValidModels(gb->memory.rom);
}
}
#endif
entry.title = NULL;
entry.base = base;
entry.filename = filename;
@ -402,14 +483,34 @@ size_t mLibraryGetEntries(struct mLibrary* library, struct mLibraryListing* out,
int i;
for (i = 0; i < nCols; ++i) {
const char* colName = sqlite3_column_name(library->select, i);
if (strcmp(colName, "crc32") == 0) {
if (strcmp(colName, "sha1") == 0) {
const void* buf = sqlite3_column_blob(library->select, i);
if (buf && sqlite3_column_bytes(library->select, i) == sizeof(entry->sha1)) {
memcpy(entry->sha1, buf, sizeof(entry->sha1));
struct NoIntroGame game;
if (!entry->title && NoIntroDBLookupGameBySHA1(library->gameDB, entry->sha1, &game)) {
entry->title = strdup(game.name);
}
}
} else if (strcmp(colName, "md5") == 0) {
const void* buf = sqlite3_column_blob(library->select, i);
if (buf && sqlite3_column_bytes(library->select, i) == sizeof(entry->md5)) {
memcpy(entry->md5, buf, sizeof(entry->md5));
struct NoIntroGame game;
if (!entry->title && NoIntroDBLookupGameByMD5(library->gameDB, entry->md5, &game)) {
entry->title = strdup(game.name);
}
}
} else if (strcmp(colName, "crc32") == 0) {
entry->crc32 = sqlite3_column_int(library->select, i);
struct NoIntroGame game;
if (NoIntroDBLookupGameByCRC(library->gameDB, entry->crc32, &game)) {
if (!entry->title && NoIntroDBLookupGameByCRC(library->gameDB, entry->crc32, &game)) {
entry->title = strdup(game.name);
}
} else if (strcmp(colName, "platform") == 0) {
entry->platform = sqlite3_column_int(library->select, i);
} else if (strcmp(colName, "models") == 0) {
entry->platformModels = sqlite3_column_int(library->select, i);
} else if (strcmp(colName, "size") == 0) {
entry->filesize = sqlite3_column_int64(library->select, i);
} else if (strcmp(colName, "internalCode") == 0 && sqlite3_column_type(library->select, i) == SQLITE_TEXT) {

View File

@ -353,6 +353,9 @@ static struct mScriptValue* _mScriptCoreChecksum(const struct mCore* core, int t
case mCHECKSUM_MD5:
size = 16;
break;
case mCHECKSUM_SHA1:
size = 20;
break;
}
if (!size) {
return &mScriptValueNull;

View File

@ -14,6 +14,8 @@
struct NoIntroDB {
sqlite3* db;
sqlite3_stmt* crc32;
sqlite3_stmt* md5;
sqlite3_stmt* sha1;
};
struct NoIntroDB* NoIntroDBLoad(const char* path) {
@ -54,8 +56,18 @@ struct NoIntroDB* NoIntroDBLoad(const char* path) {
goto error;
}
static const char selectRom[] = "SELECT * FROM games JOIN roms USING (gid) WHERE roms.crc32 = ?;";
if (sqlite3_prepare_v2(db->db, selectRom, -1, &db->crc32, NULL)) {
static const char selectCrc32[] = "SELECT games.name, roms.name, size, crc32, md5, sha1, flags FROM games JOIN roms USING (gid) WHERE roms.crc32 = ?;";
if (sqlite3_prepare_v2(db->db, selectCrc32, -1, &db->crc32, NULL)) {
goto error;
}
static const char selectMd5[] = "SELECT games.name, roms.name, size, crc32, md5, sha1, flags FROM games JOIN roms USING (gid) WHERE roms.md5 = ?;";
if (sqlite3_prepare_v2(db->db, selectMd5, -1, &db->md5, NULL)) {
goto error;
}
static const char selectSha1[] = "SELECT games.name, roms.name, size, crc32, md5, sha1, flags FROM games JOIN roms USING (gid) WHERE roms.sha1 = ?;";
if (sqlite3_prepare_v2(db->db, selectSha1, -1, &db->sha1, NULL)) {
goto error;
}
@ -300,12 +312,34 @@ void NoIntroDBDestroy(struct NoIntroDB* db) {
if (db->crc32) {
sqlite3_finalize(db->crc32);
}
if (db->md5) {
sqlite3_finalize(db->md5);
}
if (db->sha1) {
sqlite3_finalize(db->sha1);
}
if (db->db) {
sqlite3_close(db->db);
}
free(db);
}
void _extractGame(sqlite3_stmt* stmt, struct NoIntroGame* game) {
game->name = (const char*) sqlite3_column_text(stmt, 0);
game->romName = (const char*) sqlite3_column_text(stmt, 1);
game->size = sqlite3_column_int(stmt, 2);
game->crc32 = sqlite3_column_int(stmt, 3);
const void* buf = sqlite3_column_blob(stmt, 4);
if (buf && sqlite3_column_bytes(stmt, 4) == sizeof(game->md5)) {
memcpy(game->md5, buf, sizeof(game->md5));
}
buf = sqlite3_column_blob(stmt, 5);
if (buf && sqlite3_column_bytes(stmt, 5) == sizeof(game->sha1)) {
memcpy(game->sha1, buf, sizeof(game->sha1));
}
game->verified = sqlite3_column_int(stmt, 6);
}
bool NoIntroDBLookupGameByCRC(const struct NoIntroDB* db, uint32_t crc32, struct NoIntroGame* game) {
if (!db) {
return false;
@ -316,11 +350,34 @@ bool NoIntroDBLookupGameByCRC(const struct NoIntroDB* db, uint32_t crc32, struct
if (sqlite3_step(db->crc32) != SQLITE_ROW) {
return false;
}
game->name = (const char*) sqlite3_column_text(db->crc32, 1);
game->romName = (const char*) sqlite3_column_text(db->crc32, 3);
game->size = sqlite3_column_int(db->crc32, 4);
game->crc32 = sqlite3_column_int(db->crc32, 5);
// TODO: md5/sha1
game->verified = sqlite3_column_int(db->crc32, 8);
_extractGame(db->crc32, game);
return true;
}
bool NoIntroDBLookupGameByMD5(const struct NoIntroDB* db, const uint8_t* md5, struct NoIntroGame* game) {
if (!db) {
return false;
}
sqlite3_clear_bindings(db->md5);
sqlite3_reset(db->md5);
sqlite3_bind_blob(db->md5, 1, md5, 16, NULL);
if (sqlite3_step(db->md5) != SQLITE_ROW) {
return false;
}
_extractGame(db->md5, game);
return true;
}
bool NoIntroDBLookupGameBySHA1(const struct NoIntroDB* db, const uint8_t* sha1, struct NoIntroGame* game) {
if (!db) {
return false;
}
sqlite3_clear_bindings(db->sha1);
sqlite3_reset(db->sha1);
sqlite3_bind_blob(db->sha1, 1, sha1, 20, NULL);
if (sqlite3_step(db->sha1) != SQLITE_ROW) {
return false;
}
_extractGame(db->sha1, game);
return true;
}

View File

@ -27,6 +27,8 @@ struct NoIntroDB* NoIntroDBLoad(const char* path);
bool NoIntroDBLoadClrMamePro(struct NoIntroDB* db, struct VFile* vf);
void NoIntroDBDestroy(struct NoIntroDB* db);
bool NoIntroDBLookupGameByCRC(const struct NoIntroDB* db, uint32_t crc32, struct NoIntroGame* game);
bool NoIntroDBLookupGameByMD5(const struct NoIntroDB* db, const uint8_t* md5, struct NoIntroGame* game);
bool NoIntroDBLookupGameBySHA1(const struct NoIntroDB* db, const uint8_t* sha1, struct NoIntroGame* game);
CXX_GUARD_END

View File

@ -417,6 +417,7 @@ static void _compress(struct VFile* dest, struct VFile* src) {
}
dest->write(dest, compressBuffer, sizeof(compressBuffer) - zstr.avail_out);
} while (sizeof(compressBuffer) - zstr.avail_out);
deflateEnd(&zstr);
}
static bool _decompress(struct VFile* dest, struct VFile* src, size_t compressedLength) {

View File

@ -24,6 +24,7 @@
#include <mgba-util/md5.h>
#include <mgba-util/memory.h>
#include <mgba-util/patch.h>
#include <mgba-util/sha1.h>
#include <mgba-util/vfs.h>
static const struct mCoreChannelInfo _GBVideoLayers[] = {
@ -150,7 +151,7 @@ static bool _GBCoreInit(struct mCore* core) {
gbcore->keys = 0;
gb->keySource = &gbcore->keys;
#ifdef ENABLE_VFS
#if defined(ENABLE_VFS) && defined(ENABLE_DIRECTORIES)
mDirectorySetInit(&core->dirs);
#endif
@ -162,7 +163,7 @@ static void _GBCoreDeinit(struct mCore* core) {
GBDestroy(core->board);
mappedMemoryFree(core->cpu, sizeof(struct SM83Core));
mappedMemoryFree(core->board, sizeof(struct GB));
#ifdef ENABLE_VFS
#if defined(ENABLE_VFS) && defined(ENABLE_DIRECTORIES)
mDirectorySetDeinit(&core->dirs);
#endif
#ifdef ENABLE_DEBUGGERS
@ -539,6 +540,15 @@ static void _GBCoreChecksum(const struct mCore* core, void* data, enum mCoreChec
md5Buffer("", 0, data);
}
break;
case mCHECKSUM_SHA1:
if (gb->romVf) {
sha1File(gb->romVf, data);
} else if (gb->memory.rom && gb->isPristine) {
sha1Buffer(gb->memory.rom, gb->pristineRomSize, data);
} else {
sha1Buffer("", 0, data);
}
break;
}
return;
}
@ -651,6 +661,7 @@ static void _GBCoreReset(struct mCore* core) {
bios = NULL;
}
}
#if defined(ENABLE_VFS) && defined(ENABLE_DIRECTORIES)
if (!found) {
char path[PATH_MAX];
mCoreConfigDirectory(path, PATH_MAX);
@ -679,6 +690,7 @@ static void _GBCoreReset(struct mCore* core) {
bios = NULL;
}
}
#endif
if (found && bios) {
GBLoadBIOS(gb, bios);
}
@ -1128,7 +1140,7 @@ static void _GBCoreLoadSymbols(struct mCore* core, struct VFile* vf) {
if (!core->symbolTable) {
core->symbolTable = mDebuggerSymbolTableCreate();
}
#ifdef ENABLE_VFS
#if defined(ENABLE_VFS) && defined(ENABLE_DIRECTORIES)
if (!vf && core->dirs.base) {
vf = mDirectorySetOpenSuffix(&core->dirs, core->dirs.base, ".sym", O_RDONLY);
}

View File

@ -35,7 +35,9 @@ static const uint8_t _registeredTrademark[] = {0x3C, 0x42, 0xB9, 0xA5, 0xB9, 0xA
#define SGB2_BIOS_CHECKSUM 0X53D0DD63
#define CGB_BIOS_CHECKSUM 0x41884E46
#define CGB0_BIOS_CHECKSUM 0xE8EF5318
#define CGBE_BIOS_CHECKSUM 0xE95DC95D
#define AGB_BIOS_CHECKSUM 0xFFD6B0F1
#define AGB0_BIOS_CHECKSUM 0x570337EA
mLOG_DEFINE_CATEGORY(GB, "GB", "gb");
@ -85,6 +87,7 @@ static void GBInit(void* cpu, struct mCPUComponent* component) {
gb->isPristine = false;
gb->pristineRomSize = 0;
gb->yankedRomSize = 0;
gb->sramSize = 0;
memset(&gb->gbx, 0, sizeof(gb->gbx));
@ -230,7 +233,7 @@ static void GBSramDeinit(struct GB* gb) {
} else if (gb->memory.sram) {
mappedMemoryFree(gb->memory.sram, gb->sramSize);
}
gb->memory.sram = 0;
gb->memory.sram = NULL;
}
bool GBLoadSave(struct GB* gb, struct VFile* vf) {
@ -339,6 +342,7 @@ void GBResizeSram(struct GB* gb, size_t size) {
mappedMemoryFree(gb->memory.sram, gb->sramSize);
} else {
memset(newSram, 0xFF, size);
gb->sramSize = size;
}
gb->memory.sram = newSram;
}
@ -435,6 +439,8 @@ void GBUnloadROM(struct GB* gb) {
gb->memory.rom = NULL;
gb->memory.mbcType = GB_MBC_AUTODETECT;
gb->isPristine = false;
gb->pristineRomSize = 0;
gb->memory.romSize = 0;
if (!gb->sramDirty) {
gb->sramMaskWriteback = false;
@ -444,6 +450,7 @@ void GBUnloadROM(struct GB* gb) {
if (gb->sramRealVf) {
gb->sramRealVf->close(gb->sramRealVf);
}
gb->sramSize = 0;
gb->sramRealVf = NULL;
gb->sramVf = NULL;
if (gb->memory.cam && gb->memory.cam->stopRequestImage) {
@ -550,7 +557,9 @@ bool GBIsBIOS(struct VFile* vf) {
case SGB2_BIOS_CHECKSUM:
case CGB_BIOS_CHECKSUM:
case CGB0_BIOS_CHECKSUM:
case CGBE_BIOS_CHECKSUM:
case AGB_BIOS_CHECKSUM:
case AGB0_BIOS_CHECKSUM:
return true;
default:
return false;
@ -567,7 +576,9 @@ bool GBIsCompatibleBIOS(struct VFile* vf, enum GBModel model) {
return model < GB_MODEL_CGB;
case CGB_BIOS_CHECKSUM:
case CGB0_BIOS_CHECKSUM:
case CGBE_BIOS_CHECKSUM:
case AGB_BIOS_CHECKSUM:
case AGB0_BIOS_CHECKSUM:
return model >= GB_MODEL_CGB;
default:
return false;
@ -862,9 +873,11 @@ void GBDetectModel(struct GB* gb) {
break;
case CGB_BIOS_CHECKSUM:
case CGB0_BIOS_CHECKSUM:
case CGBE_BIOS_CHECKSUM:
gb->model = GB_MODEL_CGB;
break;
case AGB_BIOS_CHECKSUM:
case AGB0_BIOS_CHECKSUM:
gb->model = GB_MODEL_AGB;
break;
default:

View File

@ -274,6 +274,7 @@ void GBMBCSwitchSramHalfBank(struct GB* gb, int half, int bank) {
void GBMBCInit(struct GB* gb) {
const struct GBCartridge* cart = (const struct GBCartridge*) &gb->memory.rom[0x100];
size_t sramSize = 0;
if (gb->memory.rom && gb->memory.romSize) {
if (gb->memory.romSize >= 0x8000) {
const struct GBCartridge* cartFooter = (const struct GBCartridge*) &gb->memory.rom[gb->memory.romSize - 0x7F00];
@ -282,25 +283,25 @@ void GBMBCInit(struct GB* gb) {
}
}
if (gb->gbx.romSize) {
gb->sramSize = gb->gbx.ramSize;
sramSize = gb->gbx.ramSize;
gb->memory.mbcType = gb->gbx.mbc;
} else {
switch (cart->ramSize) {
case 0:
gb->sramSize = 0;
sramSize = 0;
break;
default:
case 2:
gb->sramSize = 0x2000;
sramSize = 0x2000;
break;
case 3:
gb->sramSize = 0x8000;
sramSize = 0x8000;
break;
case 4:
gb->sramSize = 0x20000;
sramSize = 0x20000;
break;
case 5:
gb->sramSize = 0x10000;
sramSize = 0x10000;
break;
}
}
@ -399,7 +400,7 @@ void GBMBCInit(struct GB* gb) {
gb->memory.mbcWrite = _GBMBC2;
gb->memory.mbcRead = _GBMBC2Read;
gb->memory.directSramAccess = false;
gb->sramSize = 0x100;
sramSize = 0x100;
break;
case GB_MBC3:
gb->memory.mbcWrite = _GBMBC3;
@ -414,15 +415,15 @@ void GBMBCInit(struct GB* gb) {
gb->memory.mbcWrite = _GBMBC6;
gb->memory.mbcRead = _GBMBC6Read;
gb->memory.directSramAccess = false;
if (!gb->sramSize) {
gb->sramSize = GB_SIZE_EXTERNAL_RAM; // Force minimum size for convenience
if (!sramSize) {
sramSize = GB_SIZE_EXTERNAL_RAM; // Force minimum size for convenience
}
gb->sramSize += GB_SIZE_MBC6_FLASH; // Flash is concatenated at the end
sramSize += GB_SIZE_MBC6_FLASH; // Flash is concatenated at the end
break;
case GB_MBC7:
gb->memory.mbcWrite = _GBMBC7;
gb->memory.mbcRead = _GBMBC7Read;
gb->sramSize = 0x100;
sramSize = 0x100;
break;
case GB_MMM01:
gb->memory.mbcWrite = _GBMMM01;
@ -440,7 +441,7 @@ void GBMBCInit(struct GB* gb) {
gb->memory.mbcState.tama5.rtcAlarmPage[GBTAMA6_RTC_PAGE] = 1;
gb->memory.mbcState.tama5.rtcFreePage0[GBTAMA6_RTC_PAGE] = 2;
gb->memory.mbcState.tama5.rtcFreePage1[GBTAMA6_RTC_PAGE] = 3;
gb->sramSize = 0x20;
sramSize = 0x20;
break;
case GB_MBC3_RTC:
memset(gb->memory.rtcRegs, 0, sizeof(gb->memory.rtcRegs));
@ -452,8 +453,8 @@ void GBMBCInit(struct GB* gb) {
case GB_POCKETCAM:
gb->memory.mbcWrite = _GBPocketCam;
gb->memory.mbcRead = _GBPocketCamRead;
if (!gb->sramSize) {
gb->sramSize = GB_SIZE_EXTERNAL_RAM; // Force minimum size for convenience
if (!sramSize) {
sramSize = GB_SIZE_EXTERNAL_RAM; // Force minimum size for convenience
}
if (gb->memory.cam && gb->memory.cam->startRequestImage) {
gb->memory.cam->startRequestImage(gb->memory.cam, GBCAM_WIDTH, GBCAM_HEIGHT, mCOLOR_ANY);
@ -508,7 +509,7 @@ void GBMBCInit(struct GB* gb) {
gb->memory.mbcReadBank1 = true;
gb->memory.mbcReadHigh = true;
gb->memory.mbcWriteHigh = true;
if (gb->sramSize) {
if (sramSize) {
gb->memory.sramAccess = true;
}
break;
@ -516,7 +517,7 @@ void GBMBCInit(struct GB* gb) {
gb->memory.mbcWrite = _GBSintax;
gb->memory.mbcRead = _GBSintaxRead;
gb->memory.mbcReadBank1 = true;
if (gb->sramSize) {
if (sramSize) {
gb->memory.sramAccess = true;
}
break;
@ -539,7 +540,7 @@ void GBMBCInit(struct GB* gb) {
}
memset(&gb->memory.rtcRegs, 0, sizeof(gb->memory.rtcRegs));
GBResizeSram(gb, gb->sramSize);
GBResizeSram(gb, sramSize);
if (gb->memory.mbcType == GB_MBC3_RTC) {
GBMBCRTCRead(gb);

View File

@ -650,7 +650,7 @@ void _eReaderWriteControl0(struct GBACartEReader* ereader, uint8_t value) {
}
ereader->registerControl0 = control;
if (!EReaderControl0IsScan(oldControl) && EReaderControl0IsScan(control)) {
if (ereader->scanX > 1000) {
if (ereader->scanX > 0) {
_eReaderScanCard(ereader);
}
ereader->scanX = 0;
@ -668,7 +668,7 @@ void _eReaderWriteControl1(struct GBACartEReader* ereader, uint8_t value) {
++ereader->scanY;
if (ereader->scanY == (ereader->serial[0x15] | (ereader->serial[0x14] << 8))) {
ereader->scanY = 0;
if (ereader->scanX < 3400) {
if (ereader->scanX < 4050) {
ereader->scanX += 210;
}
}

View File

@ -21,6 +21,7 @@ static void _outputPins(struct GBACartridgeHardware* hw, unsigned pins);
static void _rtcReadPins(struct GBACartridgeHardware* hw);
static unsigned _rtcOutput(struct GBACartridgeHardware* hw);
static void _rtcBeginCommand(struct GBACartridgeHardware* hw);
static void _rtcProcessByte(struct GBACartridgeHardware* hw);
static void _rtcUpdateClock(struct GBACartridgeHardware* hw);
static unsigned _rtcBCD(unsigned value);
@ -49,6 +50,7 @@ void GBAHardwareInit(struct GBACartridgeHardware* hw, uint16_t* base) {
void GBAHardwareReset(struct GBACartridgeHardware* hw) {
hw->readWrite = GPIO_WRITE_ONLY;
hw->writeLatch = 0;
hw->pinState = 0;
hw->direction = 0;
hw->lightCounter = 0;
@ -64,6 +66,7 @@ void GBAHardwareReset(struct GBACartridgeHardware* hw) {
void GBAHardwareClear(struct GBACartridgeHardware* hw) {
hw->devices = HW_NONE | (hw->devices & HW_GB_PLAYER_DETECTION);
hw->readWrite = GPIO_WRITE_ONLY;
hw->writeLatch = 0;
hw->pinState = 0;
hw->direction = 0;
}
@ -74,19 +77,25 @@ void GBAHardwareGPIOWrite(struct GBACartridgeHardware* hw, uint32_t address, uin
}
switch (address) {
case GPIO_REG_DATA:
hw->writeLatch = value & 0xF;
if (!hw->p->vbaBugCompat) {
hw->pinState &= ~hw->direction;
hw->pinState |= value & hw->direction;
hw->pinState |= hw->writeLatch & hw->direction;
} else {
hw->pinState = value;
hw->pinState = hw->writeLatch;
}
_readPins(hw);
break;
case GPIO_REG_DIRECTION:
hw->direction = value;
hw->direction = value & 0xF;
if (!hw->p->vbaBugCompat) {
hw->pinState &= ~hw->direction;
hw->pinState |= hw->writeLatch & hw->direction;
_readPins(hw);
}
break;
case GPIO_REG_CONTROL:
hw->readWrite = value;
hw->readWrite = value & 0x1;
break;
default:
mLOG(GBA_HW, WARN, "Invalid GPIO address");
@ -106,11 +115,11 @@ void GBAHardwareInitRTC(struct GBACartridgeHardware* hw) {
hw->devices |= HW_RTC;
hw->rtc.bytesRemaining = 0;
hw->rtc.transferStep = 0;
hw->rtc.bitsRead = 0;
hw->rtc.bits = 0;
hw->rtc.commandActive = 0;
hw->rtc.commandActive = false;
hw->rtc.sckEdge = true;
hw->rtc.sioOutput = true;
hw->rtc.command = 0;
hw->rtc.control = 0x40;
memset(hw->rtc.time, 0, sizeof(hw->rtc.time));
@ -138,11 +147,9 @@ void _readPins(struct GBACartridgeHardware* hw) {
}
void _outputPins(struct GBACartridgeHardware* hw, unsigned pins) {
hw->pinState &= hw->direction;
hw->pinState |= (pins & ~hw->direction & 0xF);
if (hw->readWrite) {
uint16_t old;
LOAD_16(old, 0, hw->gpioBase);
old &= hw->direction;
hw->pinState = old | (pins & ~hw->direction & 0xF);
STORE_16(hw->pinState, 0, hw->gpioBase);
}
}
@ -150,121 +157,127 @@ void _outputPins(struct GBACartridgeHardware* hw, unsigned pins) {
// == RTC
void _rtcReadPins(struct GBACartridgeHardware* hw) {
// Transfer sequence:
// P: 0 | 1 | 2 | 3
// == Initiate
// > HI | - | LO | -
// > HI | - | HI | -
// == Transfer bit (x8)
// > LO | x | HI | -
// > HI | - | HI | -
// < ?? | x | ?? | -
// == Terminate
// > - | - | LO | -
switch (hw->rtc.transferStep) {
case 0:
if ((hw->pinState & 5) == 1) {
hw->rtc.transferStep = 1;
}
break;
case 1:
if ((hw->pinState & 5) == 5) {
hw->rtc.transferStep = 2;
} else if ((hw->pinState & 5) != 1) {
hw->rtc.transferStep = 0;
}
break;
case 2:
// P: 0 - SCK | 1 - SIO | 2 - CS | 3 - Unused
// CS rising edge starts RTC transfer
// Conversely, CS falling edge aborts RTC transfer
// SCK rising edge shifts a bit from SIO into the transfer
// However, there appears to be a race condition if SIO changes at SCK rising edge
// For writing the command, the old SIO data is used in this race
// For writing command data, 0 is used in this race
// Note while CS is low, SCK is internally considered high by the RTC
// SCK falling edge shifts a bit from the transfer into SIO
// (Assuming a read command, outside of read commands SIO is held high)
// RTC keeps SCK/CS/Unused to low
_outputPins(hw, hw->pinState & 2);
if (!(hw->pinState & 4)) {
hw->rtc.bitsRead = 0;
hw->rtc.bytesRemaining = 0;
hw->rtc.commandActive = false;
hw->rtc.command = 0;
hw->rtc.sckEdge = true;
hw->rtc.sioOutput = true;
_outputPins(hw, 2);
return;
}
if (!hw->rtc.commandActive) {
_outputPins(hw, 2);
if (!(hw->pinState & 1)) {
hw->rtc.bits &= ~(1 << hw->rtc.bitsRead);
hw->rtc.bits |= ((hw->pinState & 2) >> 1) << hw->rtc.bitsRead;
} else {
if (hw->pinState & 4) {
if (!RTCCommandDataIsReading(hw->rtc.command)) {
++hw->rtc.bitsRead;
if (hw->rtc.bitsRead == 8) {
_rtcProcessByte(hw);
}
} else {
_outputPins(hw, 5 | (_rtcOutput(hw) << 1));
++hw->rtc.bitsRead;
if (hw->rtc.bitsRead == 8) {
--hw->rtc.bytesRemaining;
if (hw->rtc.bytesRemaining <= 0) {
hw->rtc.commandActive = 0;
hw->rtc.command = 0;
}
hw->rtc.bitsRead = 0;
}
}
} else {
hw->rtc.bitsRead = 0;
hw->rtc.bytesRemaining = 0;
hw->rtc.commandActive = 0;
hw->rtc.command = 0;
hw->rtc.transferStep = hw->pinState & 1;
_outputPins(hw, 1);
}
if (!hw->rtc.sckEdge && (hw->pinState & 1)) {
++hw->rtc.bitsRead;
if (hw->rtc.bitsRead == 8) {
_rtcBeginCommand(hw);
}
}
break;
}
}
void _rtcProcessByte(struct GBACartridgeHardware* hw) {
--hw->rtc.bytesRemaining;
if (!hw->rtc.commandActive) {
RTCCommandData command;
command = hw->rtc.bits;
if (RTCCommandDataGetMagic(command) == 0x06) {
hw->rtc.command = command;
hw->rtc.bytesRemaining = RTC_BYTES[RTCCommandDataGetCommand(command)];
hw->rtc.commandActive = hw->rtc.bytesRemaining > 0;
mLOG(GBA_HW, DEBUG, "Got RTC command %x", RTCCommandDataGetCommand(command));
switch (RTCCommandDataGetCommand(command)) {
case RTC_RESET:
hw->rtc.control = 0;
break;
case RTC_DATETIME:
case RTC_TIME:
_rtcUpdateClock(hw);
break;
case RTC_FORCE_IRQ:
case RTC_CONTROL:
break;
} else if (!RTCCommandDataIsReading(hw->rtc.command)) {
_outputPins(hw, 2);
if (!(hw->pinState & 1)) {
hw->rtc.bits &= ~(1 << hw->rtc.bitsRead);
hw->rtc.bits |= ((hw->pinState & 2) >> 1) << hw->rtc.bitsRead;
}
if (!hw->rtc.sckEdge && (hw->pinState & 1)) {
if ((((hw->rtc.bits >> hw->rtc.bitsRead) & 1) ^ ((hw->pinState & 2) >> 1))) {
hw->rtc.bits &= ~(1 << hw->rtc.bitsRead);
}
++hw->rtc.bitsRead;
if (hw->rtc.bitsRead == 8) {
_rtcProcessByte(hw);
}
} else {
mLOG(GBA_HW, WARN, "Invalid RTC command byte: %02X", hw->rtc.bits);
}
} else {
switch (RTCCommandDataGetCommand(hw->rtc.command)) {
case RTC_CONTROL:
hw->rtc.control = hw->rtc.bits;
break;
case RTC_FORCE_IRQ:
mLOG(GBA_HW, STUB, "Unimplemented RTC command %u", RTCCommandDataGetCommand(hw->rtc.command));
break;
if (hw->rtc.sckEdge && !(hw->pinState & 1)) {
hw->rtc.sioOutput = _rtcOutput(hw);
++hw->rtc.bitsRead;
if (hw->rtc.bitsRead == 8) {
--hw->rtc.bytesRemaining;
if (hw->rtc.bytesRemaining <= 0) {
hw->rtc.bytesRemaining = RTC_BYTES[RTCCommandDataGetCommand(hw->rtc.command)];
}
hw->rtc.bitsRead = 0;
}
}
_outputPins(hw, hw->rtc.sioOutput << 1);
}
hw->rtc.sckEdge = !!(hw->pinState & 1);
}
void _rtcBeginCommand(struct GBACartridgeHardware* hw) {
RTCCommandData command = hw->rtc.bits;
if (RTCCommandDataGetMagic(command) == 0x06) {
hw->rtc.command = command;
hw->rtc.bytesRemaining = RTC_BYTES[RTCCommandDataGetCommand(command)];
hw->rtc.commandActive = true;
mLOG(GBA_HW, DEBUG, "Got RTC command %x", RTCCommandDataGetCommand(command));
switch (RTCCommandDataGetCommand(command)) {
case RTC_RESET:
hw->rtc.control = 0;
break;
case RTC_DATETIME:
case RTC_TIME:
_rtcUpdateClock(hw);
break;
case RTC_FORCE_IRQ:
case RTC_CONTROL:
break;
}
} else {
mLOG(GBA_HW, WARN, "Invalid RTC command byte: %02X", hw->rtc.bits);
}
hw->rtc.bits = 0;
hw->rtc.bitsRead = 0;
if (!hw->rtc.bytesRemaining) {
hw->rtc.commandActive = 0;
hw->rtc.command = 0;
}
void _rtcProcessByte(struct GBACartridgeHardware* hw) {
switch (RTCCommandDataGetCommand(hw->rtc.command)) {
case RTC_CONTROL:
hw->rtc.control = hw->rtc.bits;
break;
case RTC_FORCE_IRQ:
mLOG(GBA_HW, STUB, "Unimplemented RTC command %u", RTCCommandDataGetCommand(hw->rtc.command));
break;
case RTC_RESET:
case RTC_DATETIME:
case RTC_TIME:
break;
}
hw->rtc.bits = 0;
hw->rtc.bitsRead = 0;
--hw->rtc.bytesRemaining;
if (hw->rtc.bytesRemaining <= 0) {
hw->rtc.bytesRemaining = RTC_BYTES[RTCCommandDataGetCommand(hw->rtc.command)];
}
}
unsigned _rtcOutput(struct GBACartridgeHardware* hw) {
uint8_t outputByte = 0;
if (!hw->rtc.commandActive) {
mLOG(GBA_HW, GAME_ERROR, "Attempting to use RTC without an active command");
return 0;
}
uint8_t outputByte = 0xFF;
switch (RTCCommandDataGetCommand(hw->rtc.command)) {
case RTC_CONTROL:
outputByte = hw->rtc.control;
@ -391,6 +404,7 @@ void _lightReadPins(struct GBACartridgeHardware* hw) {
struct GBALuminanceSource* lux = hw->p->luminanceSource;
mLOG(GBA_HW, DEBUG, "[SOLAR] Got reset");
hw->lightCounter = 0;
hw->lightEdge = true; // unverified (perhaps reset only happens on bit 1 rising edge?)
if (lux) {
if (lux->sample) {
lux->sample(lux);
@ -406,7 +420,7 @@ void _lightReadPins(struct GBACartridgeHardware* hw) {
hw->lightEdge = !(hw->pinState & 1);
bool sendBit = hw->lightCounter >= hw->lightSample;
_outputPins(hw, sendBit << 3);
_outputPins(hw, (sendBit << 3) | (hw->pinState & 0x7));
mLOG(GBA_HW, DEBUG, "[SOLAR] Output %u with pins %u", hw->lightCounter, hw->pinState);
}
@ -475,15 +489,20 @@ uint8_t GBAHardwareTiltRead(struct GBACartridgeHardware* hw, uint32_t address) {
void GBAHardwareSerialize(const struct GBACartridgeHardware* hw, struct GBASerializedState* state) {
GBASerializedHWFlags1 flags1 = 0;
flags1 = GBASerializedHWFlags1SetReadWrite(flags1, hw->readWrite);
STORE_16(hw->pinState, 0, &state->hw.pinState);
STORE_16(hw->direction, 0, &state->hw.pinDirection);
state->hw.writeLatch = hw->writeLatch;
state->hw.pinState = hw->pinState;
state->hw.pinDirection = hw->direction;
state->hw.devices = hw->devices;
GBASerializedHWFlags3 flags3 = 0;
flags3 = GBASerializedHWFlags3SetRtcSioOutput(flags3, hw->rtc.sioOutput);
state->hw.flags3 = flags3;
STORE_32(hw->rtc.bytesRemaining, 0, &state->hw.rtcBytesRemaining);
STORE_32(hw->rtc.transferStep, 0, &state->hw.rtcTransferStep);
STORE_32(hw->rtc.bitsRead, 0, &state->hw.rtcBitsRead);
STORE_32(hw->rtc.bits, 0, &state->hw.rtcBits);
STORE_32(hw->rtc.commandActive, 0, &state->hw.rtcCommandActive);
flags1 = GBASerializedHWFlags1SetRtcSckEdge(flags1, hw->rtc.sckEdge);
STORE_32(hw->rtc.command, 0, &state->hw.rtcCommand);
STORE_32(hw->rtc.control, 0, &state->hw.rtcControl);
memcpy(state->hw.time, hw->rtc.time, sizeof(state->hw.time));
@ -512,8 +531,9 @@ void GBAHardwareDeserialize(struct GBACartridgeHardware* hw, const struct GBASer
GBASerializedHWFlags1 flags1;
LOAD_16(flags1, 0, &state->hw.flags1);
hw->readWrite = GBASerializedHWFlags1GetReadWrite(flags1);
LOAD_16(hw->pinState, 0, &state->hw.pinState);
LOAD_16(hw->direction, 0, &state->hw.pinDirection);
hw->writeLatch = state->hw.writeLatch & 0xF;
hw->pinState = state->hw.pinState & 0xF;
hw->direction = state->hw.pinDirection & 0xF;
hw->devices = state->hw.devices;
if ((hw->devices & HW_GPIO) && hw->gpioBase) {
@ -529,11 +549,13 @@ void GBAHardwareDeserialize(struct GBACartridgeHardware* hw, const struct GBASer
}
}
hw->rtc.sioOutput = GBASerializedHWFlags3GetRtcSioOutput(state->hw.flags3);
LOAD_32(hw->rtc.bytesRemaining, 0, &state->hw.rtcBytesRemaining);
LOAD_32(hw->rtc.transferStep, 0, &state->hw.rtcTransferStep);
LOAD_32(hw->rtc.bitsRead, 0, &state->hw.rtcBitsRead);
LOAD_32(hw->rtc.bits, 0, &state->hw.rtcBits);
LOAD_32(hw->rtc.commandActive, 0, &state->hw.rtcCommandActive);
hw->rtc.sckEdge = GBASerializedHWFlags1GetRtcSckEdge(flags1);
LOAD_32(hw->rtc.command, 0, &state->hw.rtcCommand);
LOAD_32(hw->rtc.control, 0, &state->hw.rtcControl);
memcpy(hw->rtc.time, state->hw.time, sizeof(hw->rtc.time));

View File

@ -180,14 +180,25 @@ bool GBACheatAddVBALine(struct GBACheatSet* cheats, const char* line) {
return false;
}
struct mCheat* cheat = mCheatListAppend(&cheats->d.list);
cheat->address = address;
cheat->operandOffset = 0;
cheat->addressOffset = 0;
cheat->repeat = 1;
cheat->type = CHEAT_ASSIGN;
cheat->width = width;
cheat->operand = value;
if (address < GBA_BASE_ROM0 || address >= GBA_BASE_SRAM) {
struct mCheat* cheat = mCheatListAppend(&cheats->d.list);
memset(cheat, 0, sizeof(*cheat));
cheat->address = address;
cheat->operandOffset = 0;
cheat->addressOffset = 0;
cheat->repeat = 1;
cheat->type = CHEAT_ASSIGN;
cheat->width = width;
cheat->operand = value;
} else {
struct mCheatPatch* patch = mCheatPatchListAppend(&cheats->d.romPatches);
memset(patch, 0, sizeof(*patch));
patch->width = width;
patch->address = address;
patch->segment = 0;
patch->value = value;
patch->check = false;
}
return true;
}

View File

@ -31,6 +31,7 @@
#include <mgba-util/elf-read.h>
#endif
#include <mgba-util/md5.h>
#include <mgba-util/sha1.h>
#include <mgba-util/memory.h>
#include <mgba-util/patch.h>
#include <mgba-util/vfs.h>
@ -296,7 +297,7 @@ static bool _GBACoreInit(struct mCore* core) {
gbacore->proxyRenderer.logger = NULL;
#endif
#ifdef ENABLE_VFS
#if defined(ENABLE_VFS) && defined(ENABLE_DIRECTORIES)
mDirectorySetInit(&core->dirs);
#endif
@ -308,7 +309,7 @@ static void _GBACoreDeinit(struct mCore* core) {
GBADestroy(core->board);
mappedMemoryFree(core->cpu, sizeof(struct ARMCore));
mappedMemoryFree(core->board, sizeof(struct GBA));
#ifdef ENABLE_VFS
#if defined(ENABLE_VFS) && defined(ENABLE_DIRECTORIES)
mDirectorySetDeinit(&core->dirs);
#endif
#ifdef ENABLE_DEBUGGERS
@ -701,6 +702,19 @@ static void _GBACoreChecksum(const struct mCore* core, void* data, enum mCoreChe
md5Buffer("", 0, data);
}
break;
case mCHECKSUM_SHA1:
if (gba->romVf) {
sha1File(gba->romVf, data);
} else if (gba->mbVf) {
sha1File(gba->mbVf, data);
} else if (gba->memory.rom && gba->isPristine) {
sha1Buffer(gba->memory.rom, gba->pristineRomSize, data);
} else if (gba->memory.rom) {
sha1Buffer(gba->memory.rom, gba->memory.romSize, data);
} else {
sha1Buffer("", 0, data);
}
break;
}
return;
}
@ -1352,7 +1366,7 @@ static void _GBACoreLoadSymbols(struct mCore* core, struct VFile* vf) {
seek = vf->seek(vf, 0, SEEK_CUR);
vf->seek(vf, 0, SEEK_SET);
}
#ifdef ENABLE_VFS
#if defined(ENABLE_VFS) && defined(ENABLE_DIRECTORIES)
#ifdef USE_ELF
if (!vf && core->dirs.base) {
closeAfter = true;

View File

@ -15,7 +15,7 @@
#include <fcntl.h>
MGBA_EXPORT const uint32_t GBASavestateMagic = 0x01000000;
MGBA_EXPORT const uint32_t GBASavestateVersion = 0x00000007;
MGBA_EXPORT const uint32_t GBASavestateVersion = 0x00000009;
mLOG_DEFINE_CATEGORY(GBA_STATE, "GBA Savestate", "gba.serialize");

View File

@ -9,7 +9,7 @@ BasicInfo:
TitleInfo:
Category : Application
UniqueId : 0x1A1E
UniqueId : 0xD721
Option:
UseOnSD : true # true if App is to be installed to SD

View File

@ -960,6 +960,7 @@ bool retro_load_game(const struct retro_game_info* game) {
switch (gb->model) {
case GB_MODEL_AGB:
case GB_MODEL_CGB:
case GB_MODEL_SCGB:
biosName = "gbc_bios.bin";
break;
case GB_MODEL_SGB:
@ -1385,8 +1386,8 @@ static void _updateRotation(struct mRotationSource* source) {
gyroZ = 0;
_initSensors();
if (tiltEnabled) {
tiltX = sensorGetCallback(0, RETRO_SENSOR_ACCELEROMETER_X) * -2e8f;
tiltY = sensorGetCallback(0, RETRO_SENSOR_ACCELEROMETER_Y) * 2e8f;
tiltX = sensorGetCallback(0, RETRO_SENSOR_ACCELEROMETER_X) * 3e8f;
tiltY = sensorGetCallback(0, RETRO_SENSOR_ACCELEROMETER_Y) * -3e8f;
}
if (gyroEnabled) {
gyroZ = sensorGetCallback(0, RETRO_SENSOR_GYROSCOPE_Z) * -5.5e8f;

View File

@ -41,7 +41,7 @@ static void mGLContextInit(struct VideoBackend* v, WHandle handle) {
_initTex();
context->activeTex = 0;
glGenTextures(VIDEO_LAYER_MAX, context->tex);
glGenTextures(VIDEO_LAYER_MAX, context->layers);
int i;
for (i = 0; i < VIDEO_LAYER_MAX; ++i) {
glBindTexture(GL_TEXTURE_2D, context->layers[i]);

View File

@ -46,6 +46,7 @@ static uint32_t _pollInput(const struct mInputMap* map) {
SceCtrlData pad;
sceCtrlPeekBufferPositiveExt2(0, &pad, 1);
int input = mInputMapKeyBits(map, PSP2_INPUT, pad.buttons, 0);
input |= mPSP2ReadTouchLR(map);
if (pad.buttons & SCE_CTRL_UP || pad.ly < 64) {
input |= 1 << GUI_INPUT_UP;
@ -248,6 +249,7 @@ int main() {
};
sceTouchSetSamplingState(SCE_TOUCH_PORT_FRONT, SCE_TOUCH_SAMPLING_STATE_START);
sceTouchSetSamplingState(SCE_TOUCH_PORT_BACK, SCE_TOUCH_SAMPLING_STATE_START);
sceCtrlSetSamplingMode(SCE_CTRL_MODE_ANALOG_WIDE);
sceCtrlSetSamplingModeExt(SCE_CTRL_MODE_ANALOG_WIDE);
sceSysmoduleLoadModule(SCE_SYSMODULE_PHOTO_EXPORT);

View File

@ -33,6 +33,7 @@
#include <psp2/gxm.h>
#include <psp2/kernel/sysmem.h>
#include <psp2/motion.h>
#include <psp2/touch.h>
#include <vita2d.h>
@ -57,6 +58,7 @@ static double fpsRatio = 1;
static bool interframeBlending = false;
static bool sgbCrop = false;
static bool blurry = false;
static SceTouchPanelInfo panelInfo[SCE_TOUCH_PORT_MAX_NUM];
static struct mSceRotationSource {
struct mRotationSource d;
@ -290,6 +292,8 @@ uint16_t mPSP2PollInput(struct mGUIRunner* runner) {
if (angles != GBA_KEY_NONE) {
activeKeys |= 1 << angles;
}
activeKeys |= mPSP2ReadTouchLR(&runner->core->inputMap);
return activeKeys;
}
@ -313,6 +317,9 @@ void mPSP2Setup(struct mGUIRunner* runner) {
mCoreConfigSetDefaultIntValue(&runner->config, "threadedVideo", 1);
mCoreLoadForeignConfig(runner->core, &runner->config);
sceTouchGetPanelInfo(SCE_TOUCH_PORT_FRONT, &panelInfo[SCE_TOUCH_PORT_FRONT]);
sceTouchGetPanelInfo(SCE_TOUCH_PORT_BACK, &panelInfo[SCE_TOUCH_PORT_BACK]);
mPSP2MapKey(&runner->core->inputMap, SCE_CTRL_CROSS, GBA_KEY_A);
mPSP2MapKey(&runner->core->inputMap, SCE_CTRL_CIRCLE, GBA_KEY_B);
mPSP2MapKey(&runner->core->inputMap, SCE_CTRL_START, GBA_KEY_START);
@ -406,18 +413,6 @@ void mPSP2LoadROM(struct mGUIRunner* runner) {
mCoreConfigGetBoolValue(&runner->config, "interframeBlending", &interframeBlending);
// Backcompat: Old versions of mGBA use an older binding system that has different mappings for L/R
if (!sceKernelIsPSVitaTV()) {
int key = mInputMapKey(&runner->core->inputMap, PSP2_INPUT, __builtin_ctz(SCE_CTRL_L2));
if (key >= 0) {
mPSP2MapKey(&runner->core->inputMap, SCE_CTRL_L1, key);
}
key = mInputMapKey(&runner->core->inputMap, PSP2_INPUT, __builtin_ctz(SCE_CTRL_R2));
if (key >= 0) {
mPSP2MapKey(&runner->core->inputMap, SCE_CTRL_R1, key);
}
}
MutexInit(&audioContext.mutex);
ConditionInit(&audioContext.cond);
mAudioBufferClear(&audioContext.buffer);
@ -664,6 +659,39 @@ bool mPSP2SystemPoll(struct mGUIRunner* runner) {
return true;
}
int mPSP2ReadTouchLR(const struct mInputMap* map) {
SceTouchData touch[4];
int activeKeys = 0;
int touches = sceTouchPeek(SCE_TOUCH_PORT_BACK, touch, 4);
int i;
for (i = 0; i < touches; ++i) {
if (touch[i].reportNum < 1) {
continue;
}
bool left = touch[i].report[0].x < (panelInfo[SCE_TOUCH_PORT_BACK].maxAaX - panelInfo[SCE_TOUCH_PORT_BACK].minAaX) / 2;
bool top = touch[i].report[0].y < (panelInfo[SCE_TOUCH_PORT_BACK].maxAaY - panelInfo[SCE_TOUCH_PORT_BACK].minAaY) / 2;
int button;
if (left) {
if (top) {
button = __builtin_ctz(SCE_CTRL_L2);
} else {
button = __builtin_ctz(SCE_CTRL_L3);
}
} else {
if (top) {
button = __builtin_ctz(SCE_CTRL_R2);
} else {
button = __builtin_ctz(SCE_CTRL_R3);
}
}
int key = mInputMapKey(map, PSP2_INPUT, button);
if (key != -1) {
activeKeys |= 1 << key;
}
}
return activeKeys;
}
__attribute__((noreturn, weak)) void __assert_func(const char* file, int line, const char* func, const char* expr) {
printf("ASSERT FAILED: %s in %s at %s:%i\n", expr, func, file, line);
exit(1);

View File

@ -28,4 +28,6 @@ void mPSP2SetFrameLimiter(struct mGUIRunner* runner, bool limit);
uint16_t mPSP2PollInput(struct mGUIRunner* runner);
bool mPSP2SystemPoll(struct mGUIRunner* runner);
int mPSP2ReadTouchLR(const struct mInputMap* map);
#endif

View File

@ -74,7 +74,7 @@ AboutScreen::AboutScreen(QWidget* parent)
{
QString copyright = m_ui.copyright->text();
copyright.replace("{year}", QLatin1String("2023"));
copyright.replace("{year}", QLatin1String("2025"));
m_ui.copyright->setText(copyright);
}
}

View File

@ -220,6 +220,11 @@ set(GB_SRC
GBOverride.cpp
PrinterView.cpp)
set(TEST_QT_spanset_SRC
test/spanset.cpp
utils.cpp
VFileDevice.cpp)
set(CPACK_DEBIAN_PACKAGE_DEPENDS "${CPACK_DEBIAN_PACKAGE_DEPENDS},libqt${QT_V}widgets${QT_V}")
set(AUDIO_SRC)
@ -274,8 +279,15 @@ if(USE_SQLITE3)
list(APPEND SOURCE_FILES
ArchiveInspector.cpp
library/LibraryController.cpp
library/LibraryGrid.cpp
library/LibraryTree.cpp)
library/LibraryEntry.cpp
library/LibraryModel.cpp)
set(TEST_QT_library_SRC
library/LibraryEntry.cpp
library/LibraryModel.cpp
test/library.cpp
utils.cpp
VFileDevice.cpp)
endif()
if(USE_DISCORD_RPC)
@ -285,12 +297,15 @@ endif()
if(ENABLE_SCRIPTING)
list(APPEND SOURCE_FILES
scripting/AutorunScriptModel.cpp
scripting/AutorunScriptView.cpp
scripting/ScriptingController.cpp
scripting/ScriptingTextBuffer.cpp
scripting/ScriptingTextBufferModel.cpp
scripting/ScriptingView.cpp)
list(APPEND UI_FILES
scripting/AutorunScriptView.ui
scripting/ScriptingView.ui)
endif()
@ -496,7 +511,7 @@ if(APPLE)
set_source_files_properties("${COREAUDIO}" PROPERTIES MACOSX_PACKAGE_LOCATION Contents/PlugIns)
set_source_files_properties("${QTAVFSERVICE}" PROPERTIES MACOSX_PACKAGE_LOCATION Contents/PlugIns)
endif()
install(CODE "
include(BundleUtilities)
set(BU_CHMOD_BUNDLE_ITEMS ON)
@ -539,6 +554,25 @@ elseif(WIN32)
endif()
debug_strip(${BINARY_NAME}-qt)
if(BUILD_SUITE)
enable_testing()
find_package(${QT}Test)
if(${QT}Test_FOUND)
get_property(ALL_TESTS DIRECTORY PROPERTY VARIABLES)
list(FILTER ALL_TESTS INCLUDE REGEX "^TEST_QT_.*_SRC$")
foreach(TEST_SRC ${ALL_TESTS})
string(REGEX REPLACE "^TEST_QT_(.*)_SRC$" "\\1" TEST_NAME ${TEST_SRC})
add_executable(test-qt-${TEST_NAME} WIN32 ${${TEST_SRC}})
target_link_libraries(test-qt-${TEST_NAME} ${PLATFORM_LIBRARY} ${BINARY_NAME} ${QT_LIBRARIES} ${QT}::Test)
set_target_properties(test-qt-${TEST_NAME} PROPERTIES COMPILE_DEFINITIONS "${FEATURE_DEFINES};${FUNCTION_DEFINES};${OS_DEFINES};${QT_DEFINES}" COMPILE_OPTIONS "${FEATURE_FLAGS}")
add_test(platform-qt-${TEST_NAME} test-qt-${TEST_NAME})
endforeach()
else()
message(WARNING "${QT}Test not found")
endif()
endif()
install(TARGETS ${BINARY_NAME}-qt
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT ${BINARY_NAME}-qt
BUNDLE DESTINATION ${APPDIR} COMPONENT ${BINARY_NAME}-qt)

View File

@ -7,6 +7,7 @@
#include "ActionMapper.h"
#include "CoreController.h"
#include "scripting/AutorunScriptModel.h"
#include <QDir>
#include <QMenu>
@ -122,6 +123,8 @@ QString ConfigController::s_configDir;
ConfigController::ConfigController(QObject* parent)
: QObject(parent)
{
qRegisterMetaType<AutorunScriptModel::ScriptInfo>();
QString fileName = configDir();
fileName.append(QDir::separator());
fileName.append("qt.ini");
@ -379,6 +382,36 @@ void ConfigController::setMRU(const QStringList& mru, ConfigController::MRU mruT
m_settings->endGroup();
}
QList<QVariant> ConfigController::getList(const QString& group) const {
QList<QVariant> list;
m_settings->beginGroup(group);
for (int i = 0; ; ++i) {
QVariant item = m_settings->value(QString::number(i));
if (item.isNull() || !item.isValid()) {
break;
}
list.append(item);
}
m_settings->endGroup();
return list;
}
void ConfigController::setList(const QString& group, const QList<QVariant>& list) {
int i = 0;
m_settings->beginGroup(group);
QStringList keys = m_settings->childKeys();
for (const QVariant& item : list) {
QString key = QString::number(i);
keys.removeAll(key);
m_settings->setValue(key, item);
++i;
}
for (const auto& key: keys) {
m_settings->remove(key);
}
m_settings->endGroup();
}
constexpr const char* ConfigController::mruName(ConfigController::MRU mru) {
switch (mru) {
case MRU::ROM:

View File

@ -90,7 +90,7 @@ public:
QVariant takeArgvOption(const QString& key);
QStringList getMRU(MRU = MRU::ROM) const;
void setMRU(const QStringList& mru, MRU = MRU::ROM);
QList<QVariant> getList(const QString& group) const;
Configuration* overrides() { return mCoreConfigGetOverrides(&m_config); }
void saveOverride(const Override&);
@ -116,6 +116,8 @@ public slots:
void setOption(const char* key, const char* value);
void setOption(const char* key, const QVariant& value);
void setQtOption(const QString& key, const QVariant& value, const QString& group = QString());
void setMRU(const QStringList& mru, MRU = MRU::ROM);
void setList(const QString& group, const QList<QVariant>& list);
void makePortable();
void write();

View File

@ -205,7 +205,8 @@ CoreController::CoreController(mCore* core, QObject* parent)
}
message = QString::vasprintf(format, args);
QMetaObject::invokeMethod(controller, "logPosted", Q_ARG(int, level), Q_ARG(int, category), Q_ARG(const QString&, message));
if (level == mLOG_FATAL) {
if (level == mLOG_FATAL && !controller->m_crashSeen) {
controller->m_crashSeen = true;
QMetaObject::invokeMethod(controller, "crashed", Q_ARG(const QString&, message));
}
};
@ -500,6 +501,7 @@ void CoreController::stop() {
}
void CoreController::reset() {
m_crashSeen = false;
mCoreThreadReset(&m_threadContext);
}
@ -651,6 +653,7 @@ void CoreController::loadState(int slot) {
m_stateSlot = slot;
m_backupSaveState.clear();
}
m_crashSeen = false;
mCoreThreadClearCrashed(&m_threadContext);
mCoreThreadRunFunction(&m_threadContext, [](mCoreThread* context) {
CoreController* controller = static_cast<CoreController*>(context->userData);
@ -671,6 +674,7 @@ void CoreController::loadState(const QString& path, int flags) {
if (flags != -1) {
m_loadStateFlags = flags;
}
m_crashSeen = false;
mCoreThreadClearCrashed(&m_threadContext);
mCoreThreadRunFunction(&m_threadContext, [](mCoreThread* context) {
CoreController* controller = static_cast<CoreController*>(context->userData);
@ -700,6 +704,7 @@ void CoreController::loadState(QIODevice* iodev, int flags) {
if (flags != -1) {
m_loadStateFlags = flags;
}
m_crashSeen = false;
mCoreThreadClearCrashed(&m_threadContext);
mCoreThreadRunFunction(&m_threadContext, [](mCoreThread* context) {
CoreController* controller = static_cast<CoreController*>(context->userData);

View File

@ -257,6 +257,7 @@ private:
struct CoreLogger : public mLogger {
CoreController* self;
} m_logger{};
bool m_crashSeen = false;
QString m_path;
QString m_baseDirectory;

View File

@ -699,7 +699,13 @@ void PainterGL::resizeContext() {
dequeueAll(false);
mRectangle dims = {0, 0, size.width(), size.height()};
if (!m_started) {
makeCurrent();
}
m_backend->setLayerDimensions(m_backend, VIDEO_LAYER_IMAGE, &dims);
if (!m_started) {
m_gl->doneCurrent();
}
recenterLayers();
m_dims = size;
}
@ -807,11 +813,11 @@ void PainterGL::start() {
mGLES2ShaderAttach(reinterpret_cast<mGLES2Context*>(m_backend), static_cast<mGLES2Shader*>(m_shader.passes), m_shader.nPasses);
}
#endif
resizeContext();
m_buffer = nullptr;
m_active = true;
m_started = true;
resizeContext();
swapInterval(1);
emit started();
}

View File

@ -594,7 +594,16 @@ void FrameView::exportFrame() {
return;
}
CoreController::Interrupter interrupter(m_controller);
m_framebuffer.save(filename, "PNG");
unsigned width, height;
m_vl->currentVideoSize(m_vl, &width, &height);
if ((int)width != m_framebuffer.width() || (int)height != m_framebuffer.height()) {
QImage crop = m_framebuffer.copy(0, 0, width, height);
crop.save(filename, "PNG");
} else {
m_framebuffer.save(filename, "PNG");
}
}
void FrameView::reset() {

View File

@ -15,7 +15,7 @@
using namespace QGBA;
GIFView::GIFView(QWidget* parent)
GIFView::GIFView(std::shared_ptr<CoreController> controller, QWidget* parent)
: QWidget(parent)
{
m_ui.setupUi(this);
@ -31,6 +31,8 @@ GIFView::GIFView(QWidget* parent)
FFmpegEncoderInit(&m_encoder);
FFmpegEncoderSetAudio(&m_encoder, nullptr, 0);
setController(controller);
}
GIFView::~GIFView() {

View File

@ -23,7 +23,7 @@ class GIFView : public QWidget {
Q_OBJECT
public:
GIFView(QWidget* parent = nullptr);
GIFView(std::shared_ptr<CoreController> controller, QWidget* parent = nullptr);
virtual ~GIFView();
mAVStream* getStream() { return &m_encoder.d; }

View File

@ -79,31 +79,31 @@ QString GameBoy::mbcName(GBMemoryBankControllerType mbc) {
if (s_mbcNames.isEmpty()) {
s_mbcNames[GB_MBC_AUTODETECT] = tr("Autodetect");
s_mbcNames[GB_MBC_NONE] = tr("ROM Only");
s_mbcNames[GB_MBC1] = tr("MBC1");
s_mbcNames[GB_MBC2] = tr("MBC2");
s_mbcNames[GB_MBC3] = tr("MBC3");
s_mbcNames[GB_MBC3_RTC] = tr("MBC3 + RTC");
s_mbcNames[GB_MBC5] = tr("MBC5");
s_mbcNames[GB_MBC5_RUMBLE] = tr("MBC5 + Rumble");
s_mbcNames[GB_MBC6] = tr("MBC6");
s_mbcNames[GB_MBC7] = tr("MBC7 (Tilt)");
s_mbcNames[GB_MMM01] = tr("MMM01");
s_mbcNames[GB_HuC1] = tr("HuC-1");
s_mbcNames[GB_HuC3] = tr("HuC-3");
s_mbcNames[GB_POCKETCAM] = tr("Pocket Cam");
s_mbcNames[GB_TAMA5] = tr("TAMA5");
s_mbcNames[GB_UNL_WISDOM_TREE] = tr("Wisdom Tree");
s_mbcNames[GB_UNL_NT_OLD_1] = tr("NT (old 1)");
s_mbcNames[GB_UNL_NT_OLD_2] = tr("NT (old 2)");
s_mbcNames[GB_UNL_NT_NEW] = tr("NT (new)");
s_mbcNames[GB_UNL_PKJD] = tr("Pokémon Jade/Diamond");
s_mbcNames[GB_UNL_BBD] = tr("BBD");
s_mbcNames[GB_UNL_HITEK] = tr("Hitek");
s_mbcNames[GB_UNL_GGB81] = tr("GGB-81");
s_mbcNames[GB_UNL_LI_CHENG] = tr("Li Cheng");
s_mbcNames[GB_UNL_SACHEN_MMC1] = tr("Sachen (MMC1)");
s_mbcNames[GB_UNL_SACHEN_MMC2] = tr("Sachen (MMC2)");
s_mbcNames[GB_UNL_SINTAX] = tr("Sintax");
s_mbcNames[GB_MBC1] = "MBC1";
s_mbcNames[GB_MBC2] = "MBC2";
s_mbcNames[GB_MBC3] = "MBC3";
s_mbcNames[GB_MBC3_RTC] = tr("%1 + RTC").arg("MBC3");
s_mbcNames[GB_MBC5] = "MBC5";
s_mbcNames[GB_MBC5_RUMBLE] = tr("%1 + Rumble").arg("MBC5");
s_mbcNames[GB_MBC6] = "MBC6";
s_mbcNames[GB_MBC7] = tr("%1 (Tilt)").arg("MBC7");
s_mbcNames[GB_MMM01] = "MMM01";
s_mbcNames[GB_HuC1] = "HuC-1";
s_mbcNames[GB_HuC3] = "HuC-3";
s_mbcNames[GB_POCKETCAM] = "Pocket Cam";
s_mbcNames[GB_TAMA5] = "TAMA5";
s_mbcNames[GB_UNL_WISDOM_TREE] = "Wisdom Tree";
s_mbcNames[GB_UNL_NT_OLD_1] = tr("%1 (old 1)").arg("NT");
s_mbcNames[GB_UNL_NT_OLD_2] = tr("%1 (old 2)").arg("NT");
s_mbcNames[GB_UNL_NT_NEW] = tr("%1 (new)").arg("NT");
s_mbcNames[GB_UNL_PKJD] = "Pokémon Jade/Diamond";
s_mbcNames[GB_UNL_BBD] = "BBD";
s_mbcNames[GB_UNL_HITEK] = "Hitek";
s_mbcNames[GB_UNL_GGB81] = "GGB-81";
s_mbcNames[GB_UNL_LI_CHENG] = "Li Cheng";
s_mbcNames[GB_UNL_SACHEN_MMC1] = "Sachen (MMC1)";
s_mbcNames[GB_UNL_SACHEN_MMC2] = "Sachen (MMC2)";
s_mbcNames[GB_UNL_SINTAX] = "Sintax";
}
return s_mbcNames[mbc];

View File

@ -56,7 +56,7 @@ MapView::MapView(std::shared_ptr<CoreController> controller, QWidget* parent)
#ifdef M_CORE_GB
case mPLATFORM_GB:
m_boundary = 1024;
m_ui.tile->setMaxTile(512);
m_ui.tile->setMaxTile(1024);
m_addressBase = GB_BASE_VRAM;
m_addressWidth = 4;
m_ui.bgInfo->addCustomProperty("screenBase", tr("Map base"));

View File

@ -80,13 +80,17 @@ MemoryModel::MemoryModel(QWidget* parent)
setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
int hintWidth;
m_margins = QMargins(3, m_cellHeight + 1, 3, 0);
#if (QT_VERSION >= QT_VERSION_CHECK(5, 11, 0))
m_margins += QMargins(metrics.horizontalAdvance("0FFFFFF0 "), 0, metrics.horizontalAdvance(" AAAAAAAAAAAAAAAA"), 0);
hintWidth = metrics.horizontalAdvance(" 0FFFFFF0 FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF AAAAAAAAAAAAAAAA");
#else
m_margins += QMargins(metrics.width("0FFFFFF0 "), 0, metrics.width(" AAAAAAAAAAAAAAAA"), 0);
hintWidth = metrics.width(" 0FFFFFF0 FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF AAAAAAAAAAAAAAAA");
#endif
m_cellSize = QSizeF((viewport()->size().width() - (m_margins.left() + m_margins.right())) / 16.0, m_cellHeight);
setMinimumWidth(hintWidth);
connect(verticalScrollBar(), &QSlider::sliderMoved, [this](int position) {
m_top = position;

View File

@ -17,17 +17,11 @@
<item>
<widget class="QGBA::MemoryModel" name="hexfield" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>200</width>
<height>0</height>
</size>
</property>
</widget>
</item>
<item>

View File

@ -39,6 +39,10 @@ bool glContextHasBug(OpenGLBug bug) {
if (renderer == "Intel Pineview Platform") {
return true;
}
if (version == "2.1.0 - Build 8.15.10.2900") {
return true;
}
#endif
return false;

View File

@ -15,6 +15,15 @@
using namespace QGBA;
template<size_t N> bool isZeroed(const uint8_t* mem) {
for (size_t i = 0; i < N; ++i) {
if (mem[i]) {
return false;
}
}
return true;
}
ROMInfo::ROMInfo(std::shared_ptr<CoreController> controller, QWidget* parent)
: QDialog(parent, Qt::WindowTitleHint | Qt::WindowSystemMenuHint | Qt::WindowCloseButtonHint)
{
@ -25,6 +34,7 @@ ROMInfo::ROMInfo(std::shared_ptr<CoreController> controller, QWidget* parent)
#endif
uint32_t crc32 = 0;
uint8_t md5[16]{};
uint8_t sha1[20]{};
CoreController::Interrupter interrupter(controller);
mCore* core = controller->thread()->core;
@ -41,33 +51,48 @@ ROMInfo::ROMInfo(std::shared_ptr<CoreController> controller, QWidget* parent)
core->checksum(core, &crc32, mCHECKSUM_CRC32);
core->checksum(core, &md5, mCHECKSUM_MD5);
core->checksum(core, &sha1, mCHECKSUM_SHA1);
m_ui.size->setText(QString::number(core->romSize(core)) + tr(" bytes"));
if (crc32) {
m_ui.crc->setText(QString::number(crc32, 16));
#ifdef USE_SQLITE3
if (db) {
NoIntroGame game{};
if (NoIntroDBLookupGameByCRC(db, crc32, &game)) {
m_ui.name->setText(game.name);
} else {
m_ui.name->setText(tr("(unknown)"));
}
} else {
m_ui.name->setText(tr("(no database present)"));
}
#else
m_ui.name->hide();
#endif
} else {
m_ui.crc->setText(tr("(unknown)"));
m_ui.name->setText(tr("(unknown)"));
}
m_ui.md5->setText(QString::asprintf("%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x",
md5[0x0], md5[0x1], md5[0x2], md5[0x3], md5[0x4], md5[0x5], md5[0x6], md5[0x7],
md5[0x8], md5[0x9], md5[0xA], md5[0xB], md5[0xC], md5[0xD], md5[0xE], md5[0xF]));
if (!isZeroed<16>(md5)) {
m_ui.md5->setText(QString::asprintf("%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x",
md5[0x0], md5[0x1], md5[0x2], md5[0x3], md5[0x4], md5[0x5], md5[0x6], md5[0x7],
md5[0x8], md5[0x9], md5[0xA], md5[0xB], md5[0xC], md5[0xD], md5[0xE], md5[0xF]));
} else {
m_ui.md5->setText(tr("(unknown)"));
}
if (!isZeroed<20>(sha1)) {
m_ui.sha1->setText(QString::asprintf("%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x",
sha1[ 0], sha1[ 1], sha1[ 2], sha1[ 3], sha1[ 4], sha1[ 5], sha1[ 6], sha1[ 7], sha1[ 8], sha1[ 9],
sha1[10], sha1[11], sha1[12], sha1[13], sha1[14], sha1[15], sha1[16], sha1[17], sha1[18], sha1[19]));
} else {
m_ui.sha1->setText(tr("(unknown)"));
}
#ifdef USE_SQLITE3
if (db) {
NoIntroGame game{};
if (!isZeroed<20>(sha1) && NoIntroDBLookupGameBySHA1(db, sha1, &game)) {
m_ui.name->setText(game.name);
} else if (crc32 && NoIntroDBLookupGameByCRC(db, crc32, &game)) {
m_ui.name->setText(game.name);
} else {
m_ui.name->setText(tr("(unknown)"));
}
} else {
m_ui.name->setText(tr("(no database present)"));
}
#else
m_ui.name->hide();
#endif
QString savePath = controller->savePath();
if (!savePath.isEmpty()) {

View File

@ -95,13 +95,30 @@
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_10">
<property name="text">
<string>SHA-1</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QLabel" name="sha1">
<property name="text">
<string notr="true">{SHA1}</string>
</property>
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="label_6">
<property name="text">
<string>Save file:</string>
</property>
</widget>
</item>
<item row="4" column="1">
<item row="5" column="1">
<widget class="QLabel" name="savefile">
<property name="text">
<string notr="true">{SAVEFILE}</string>

View File

@ -395,7 +395,7 @@ SaveConverter::AnnotatedSave::AnnotatedSave(mPlatform platform, std::shared_ptr<
: container(container)
, platform(platform)
, size(vf->size())
, backing(vf)
, backing(std::move(vf))
, endianness(endianness)
{
}
@ -405,7 +405,7 @@ SaveConverter::AnnotatedSave::AnnotatedSave(GBASavedataType type, std::shared_pt
: container(container)
, platform(mPLATFORM_GBA)
, size(vf->size())
, backing(vf)
, backing(std::move(vf))
, endianness(endianness)
, gba({type})
{
@ -417,7 +417,7 @@ SaveConverter::AnnotatedSave::AnnotatedSave(GBMemoryBankControllerType type, std
: container(container)
, platform(mPLATFORM_GB)
, size(vf->size())
, backing(vf)
, backing(std::move(vf))
, endianness(endianness)
, gb({type})
{

View File

@ -69,6 +69,8 @@ SettingsView::SettingsView(ConfigController* controller, InputController* inputC
reloadConfig();
connect(m_ui.autorunScripts, &QAbstractButton::pressed, this, &SettingsView::openAutorunScripts);
connect(m_ui.volume, static_cast<void (QSlider::*)(int)>(&QSlider::valueChanged), [this](int v) {
if (v < m_ui.volumeFf->value()) {
m_ui.volumeFf->setValue(v);

View File

@ -63,6 +63,7 @@ signals:
void languageChanged();
void libraryCleared();
void saveSettingsRequested();
void openAutorunScripts();
public slots:
void selectPage(Page);

View File

@ -569,14 +569,21 @@
</property>
</widget>
</item>
<item row="6" column="0" colspan="2">
<item row="6" column="1">
<widget class="QPushButton" name="autorunScripts">
<property name="text">
<string>Edit autorun scripts</string>
</property>
</widget>
</item>
<item row="7" column="0" colspan="2">
<widget class="Line" name="line_9">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item row="7" column="1">
<item row="8" column="1">
<widget class="QCheckBox" name="autosave">
<property name="text">
<string>Periodically autosave state</string>
@ -586,7 +593,7 @@
</property>
</widget>
</item>
<item row="8" column="1">
<item row="9" column="1">
<widget class="QCheckBox" name="cheatAutosave">
<property name="text">
<string>Save entered cheats</string>
@ -596,21 +603,21 @@
</property>
</widget>
</item>
<item row="9" column="0" colspan="2">
<item row="10" column="0" colspan="2">
<widget class="Line" name="line_21">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item row="10" column="0">
<item row="11" column="0">
<widget class="QLabel" name="label_51">
<property name="text">
<string>Save state extra data:</string>
</property>
</widget>
</item>
<item row="10" column="1">
<item row="11" column="1">
<widget class="QCheckBox" name="saveStateScreenshot">
<property name="text">
<string>Screenshot</string>
@ -620,7 +627,7 @@
</property>
</widget>
</item>
<item row="11" column="1">
<item row="12" column="1">
<widget class="QCheckBox" name="saveStateSave">
<property name="text">
<string>Save game</string>
@ -630,7 +637,7 @@
</property>
</widget>
</item>
<item row="12" column="1">
<item row="13" column="1">
<widget class="QCheckBox" name="saveStateCheats">
<property name="text">
<string>Cheat codes</string>
@ -640,21 +647,21 @@
</property>
</widget>
</item>
<item row="13" column="0" colspan="2">
<item row="14" column="0" colspan="2">
<widget class="Line" name="line_22">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item row="14" column="0">
<item row="15" column="0">
<widget class="QLabel" name="label_52">
<property name="text">
<string>Load state extra data:</string>
</property>
</widget>
</item>
<item row="14" column="1">
<item row="15" column="1">
<widget class="QCheckBox" name="loadStateScreenshot">
<property name="text">
<string>Screenshot</string>
@ -664,28 +671,28 @@
</property>
</widget>
</item>
<item row="15" column="1">
<item row="16" column="1">
<widget class="QCheckBox" name="loadStateSave">
<property name="text">
<string>Save game</string>
</property>
</widget>
</item>
<item row="16" column="1">
<item row="17" column="1">
<widget class="QCheckBox" name="loadStateCheats">
<property name="text">
<string>Cheat codes</string>
</property>
</widget>
</item>
<item row="17" column="0" colspan="2">
<item row="18" column="0" colspan="2">
<widget class="Line" name="line_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item row="18" column="1">
<item row="19" column="1">
<widget class="QCheckBox" name="useDiscordPresence">
<property name="text">
<string>Enable Discord Rich Presence</string>

View File

@ -61,7 +61,7 @@ TileView::TileView(std::shared_ptr<CoreController> controller, QWidget* parent)
m_ui.tilesBoth->setEnabled(false);
m_ui.palette256->setEnabled(false);
m_ui.tile->setBoundary(1024, 0, 0);
m_ui.tile->setMaxTile(512);
m_ui.tile->setMaxTile(896);
break;
#endif
default:
@ -201,7 +201,8 @@ void TileView::updateTilesGBA(bool force) {
#ifdef M_CORE_GB
void TileView::updateTilesGB(bool force) {
const GB* gb = static_cast<const GB*>(m_controller->thread()->core->board);
int count = gb->model >= GB_MODEL_CGB ? 1024 : 512;
// TODO: Strip out tiles 384-511, as they aren't valid
int count = gb->model >= GB_MODEL_CGB ? 896 : 384;
m_ui.tiles->setTileCount(count);
mTileCache* cache = mTileCacheSetGetPointer(&m_cacheSet->tiles, 0);
for (int i = 0; i < count; ++i) {

View File

@ -47,7 +47,7 @@ bool VideoView::Preset::compatible(const Preset& other) const {
return true;
}
VideoView::VideoView(QWidget* parent)
VideoView::VideoView(std::shared_ptr<CoreController> controller, QWidget* parent)
: QWidget(parent)
{
m_ui.setupUi(this);
@ -65,6 +65,7 @@ VideoView::VideoView(QWidget* parent)
s_vcodecMap["hevc"] = "libx265";
s_vcodecMap["hevc nvenc"] = "hevc_nvenc";
s_vcodecMap["theora"] = "libtheora";
s_vcodecMap["ut video"] = "utvideo";
s_vcodecMap["vp8"] = "libvpx";
s_vcodecMap["vp9"] = "libvpx-vp9";
s_vcodecMap["xvid"] = "libxvid";
@ -132,6 +133,8 @@ VideoView::VideoView(QWidget* parent)
m_ui.presetYoutube->setChecked(true); // Use the Youtube preset by default
showAdvanced(false);
setController(controller);
}
void VideoView::updatePresets() {

View File

@ -26,7 +26,7 @@ class VideoView : public QWidget {
Q_OBJECT
public:
VideoView(QWidget* parent = nullptr);
VideoView(std::shared_ptr<CoreController> controller, QWidget* parent = nullptr);
virtual ~VideoView();
mAVStream* getStream() { return &m_encoder.d; }

View File

@ -302,6 +302,11 @@
<string notr="true">VP9</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">Ut Video</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">FFV1</string>

View File

@ -305,6 +305,10 @@ void Window::loadConfig() {
updateMRU();
m_inputController.setConfiguration(m_config);
if (!m_config->getList("autorunSettings").isEmpty()) {
ensureScripting();
}
}
void Window::reloadConfig() {
@ -550,6 +554,10 @@ void Window::openSettingsWindow(SettingsView::Page page) {
#ifdef USE_SQLITE3
connect(settingsWindow, &SettingsView::libraryCleared, m_libraryView, &LibraryController::clear);
#endif
connect(settingsWindow, &SettingsView::openAutorunScripts, this, [this]() {
ensureScripting();
m_scripting->openAutorunEdit();
});
connect(this, &Window::shaderSelectorAdded, settingsWindow, &SettingsView::setShaderSelector);
openView(settingsWindow);
settingsWindow->selectPage(page);
@ -580,29 +588,35 @@ std::function<void()> Window::openControllerTView(A... arg) {
}
template <typename T, typename... A>
std::function<void()> Window::openNamedTView(std::unique_ptr<T>* name, A... arg) {
std::function<void()> Window::openNamedTView(QPointer<T>* name, bool keepalive, A... arg) {
return [=]() {
if (!*name) {
*name = std::make_unique<T>(arg...);
connect(this, &Window::shutdown, name->get(), &QWidget::close);
*name = new T(arg...);
connect(this, &Window::shutdown, name->data(), &QWidget::close);
if (!keepalive) {
(*name)->setAttribute(Qt::WA_DeleteOnClose);
}
}
(*name)->show();
(*name)->setFocus(Qt::PopupFocusReason);
(*name)->activateWindow();
(*name)->raise();
};
}
template <typename T, typename... A>
std::function<void()> Window::openNamedControllerTView(std::unique_ptr<T>* name, A... arg) {
std::function<void()> Window::openNamedControllerTView(QPointer<T>* name, bool keepalive, A... arg) {
return [=]() {
if (!*name) {
*name = std::make_unique<T>(arg...);
if (m_controller) {
(*name)->setController(m_controller);
*name = new T(m_controller, arg...);
connect(m_controller.get(), &CoreController::stopping, name->data(), &QWidget::close);
connect(this, &Window::shutdown, name->data(), &QWidget::close);
if (!keepalive) {
(*name)->setAttribute(Qt::WA_DeleteOnClose);
}
connect(this, &Window::shutdown, name->get(), &QWidget::close);
}
(*name)->show();
(*name)->setFocus(Qt::PopupFocusReason);
(*name)->activateWindow();
(*name)->raise();
};
}
@ -633,17 +647,7 @@ void Window::consoleOpen() {
#ifdef ENABLE_SCRIPTING
void Window::scriptingOpen() {
if (!m_scripting) {
m_scripting = std::make_unique<ScriptingController>();
m_scripting->setInputController(&m_inputController);
m_shortcutController->setScriptingController(m_scripting.get());
if (m_controller) {
m_scripting->setController(m_controller);
m_display->installEventFilter(m_scripting.get());
}
m_scripting->setVideoBackend(m_display->videoBackend());
}
ensureScripting();
ScriptingView* view = new ScriptingView(m_scripting.get(), m_config);
openView(view);
}
@ -1101,7 +1105,7 @@ void Window::reloadDisplayDriver() {
if (!proxy) {
proxy = std::make_shared<VideoProxy>();
}
m_display->setVideoProxy(proxy);
m_display->setVideoProxy(std::move(proxy));
#ifdef ENABLE_SCRIPTING
if (m_scripting) {
m_scripting->setVideoBackend(m_display->videoBackend());
@ -1414,7 +1418,7 @@ void Window::setupMenu(QMenuBar* menubar) {
m_multiWindow = m_actions.addAction(tr("New multiplayer window"), "multiWindow", GBAApp::app(), &GBAApp::newWindow, "file");
#ifdef M_CORE_GBA
auto dolphin = m_actions.addAction(tr("Connect to Dolphin..."), "connectDolphin", openNamedTView<DolphinConnector>(&m_dolphinView, this), "file");
auto dolphin = m_actions.addAction(tr("Connect to Dolphin..."), "connectDolphin", openNamedTView<DolphinConnector>(&m_dolphinView, true, this), "file");
m_platformActions.insert(mPLATFORM_GBA, dolphin);
#endif
@ -1695,8 +1699,8 @@ void Window::setupMenu(QMenuBar* menubar) {
#endif
#ifdef USE_FFMPEG
addGameAction(tr("Record A/V..."), "recordOutput", openNamedControllerTView<VideoView>(&m_videoView), "av");
addGameAction(tr("Record GIF/WebP/APNG..."), "recordGIF", openNamedControllerTView<GIFView>(&m_gifView), "av");
addGameAction(tr("Record A/V..."), "recordOutput", openNamedControllerTView<VideoView>(&m_videoView, true), "av");
addGameAction(tr("Record GIF/WebP/APNG..."), "recordGIF", openNamedControllerTView<GIFView>(&m_gifView, true), "av");
#endif
m_actions.addSeparator("av");
@ -1710,17 +1714,29 @@ void Window::setupMenu(QMenuBar* menubar) {
m_actions.addAction(tr("Game &overrides..."), "overrideWindow", [this]() {
if (!m_overrideView) {
m_overrideView = std::make_unique<OverrideView>(m_config);
m_overrideView = new OverrideView(m_config);
if (m_controller) {
m_overrideView->setController(m_controller);
}
connect(this, &Window::shutdown, m_overrideView.get(), &QWidget::close);
connect(this, &Window::shutdown, m_overrideView.data(), &QWidget::close);
}
m_overrideView->show();
m_overrideView->recheck();
m_overrideView->activateWindow();
m_overrideView->raise();
}, "tools");
m_actions.addAction(tr("Game Pak sensors..."), "sensorWindow", openNamedControllerTView<SensorView>(&m_sensorView, &m_inputController), "tools");
m_actions.addAction(tr("Game Pak sensors..."), "sensorWindow", [this]() {
if (!m_sensorView) {
m_sensorView = new SensorView(&m_inputController);
if (m_controller) {
m_sensorView->setController(m_controller);
}
connect(this, &Window::shutdown, m_sensorView.data(), &QWidget::close);
}
m_sensorView->show();
m_sensorView->activateWindow();
m_sensorView->raise();
}, "tools");
addGameAction(tr("&Cheats..."), "cheatsWindow", openControllerTView<CheatsView>(), "tools");
#ifdef ENABLE_SCRIPTING
@ -1750,23 +1766,7 @@ void Window::setupMenu(QMenuBar* menubar) {
addGameAction(tr("View &sprites..."), "spriteWindow", openControllerTView<ObjView>(), "stateViews");
addGameAction(tr("View &tiles..."), "tileWindow", openControllerTView<TileView>(), "stateViews");
addGameAction(tr("View &map..."), "mapWindow", openControllerTView<MapView>(), "stateViews");
addGameAction(tr("&Frame inspector..."), "frameWindow", [this]() {
if (!m_frameView) {
m_frameView = new FrameView(m_controller);
connect(this, &Window::shutdown, this, [this]() {
if (m_frameView) {
m_frameView->close();
}
});
connect(m_frameView, &QObject::destroyed, this, [this]() {
m_frameView = nullptr;
});
m_frameView->setAttribute(Qt::WA_DeleteOnClose);
}
m_frameView->show();
}, "stateViews");
addGameAction(tr("&Frame inspector..."), "frameWindow", openNamedControllerTView<FrameView>(&m_frameView, false), "stateViews");
addGameAction(tr("View memory..."), "memoryView", openControllerTView<MemoryView>(), "stateViews");
addGameAction(tr("Search memory..."), "memorySearch", openControllerTView<MemorySearch>(), "stateViews");
addGameAction(tr("View &I/O registers..."), "ioViewer", openControllerTView<IOViewer>(), "stateViews");
@ -2055,6 +2055,25 @@ void Window::updateMRU() {
m_actions.rebuildMenu(menuBar(), this, *m_shortcutController);
}
void Window::ensureScripting() {
if (m_scripting) {
return;
}
m_scripting = std::make_unique<ScriptingController>(m_config);
m_scripting->setInputController(&m_inputController);
m_shortcutController->setScriptingController(m_scripting.get());
if (m_controller) {
m_scripting->setController(m_controller);
m_display->installEventFilter(m_scripting.get());
}
if (m_display) {
m_scripting->setVideoBackend(m_display->videoBackend());
}
connect(m_scripting.get(), &ScriptingController::autorunScriptsOpened, this, &Window::openView);
}
std::shared_ptr<Action> Window::addGameAction(const QString& visibleName, const QString& name, Action::Function function, const QString& menu, const QKeySequence& shortcut) {
auto action = m_actions.addAction(visibleName, name, [this, function = std::move(function)]() {
if (m_controller) {

View File

@ -109,6 +109,8 @@ public slots:
void startVideoLog();
void openView(QWidget* widget);
#ifdef ENABLE_DEBUGGERS
void consoleOpen();
#endif
@ -173,12 +175,12 @@ private:
void clearMRU();
void updateMRU();
void openView(QWidget* widget);
void ensureScripting();
template <typename T, typename... A> std::function<void()> openTView(A... arg);
template <typename T, typename... A> std::function<void()> openControllerTView(A... arg);
template <typename T, typename... A> std::function<void()> openNamedTView(std::unique_ptr<T>*, A... arg);
template <typename T, typename... A> std::function<void()> openNamedControllerTView(std::unique_ptr<T>*, A... arg);
template <typename T, typename... A> std::function<void()> openNamedTView(QPointer<T>*, bool keepalive, A... arg);
template <typename T, typename... A> std::function<void()> openNamedControllerTView(QPointer<T>*, bool keepalive, A... arg);
std::shared_ptr<Action> addGameAction(const QString& visibleName, const QString& name, Action::Function action, const QString& menu = {}, const QKeySequence& = {});
template<typename T, typename V> std::shared_ptr<Action> addGameAction(const QString& visibleName, const QString& name, T* obj, V (T::*action)(), const QString& menu = {}, const QKeySequence& = {});
@ -241,14 +243,14 @@ private:
bool m_multiActive = true;
int m_playerId;
std::unique_ptr<OverrideView> m_overrideView;
std::unique_ptr<SensorView> m_sensorView;
std::unique_ptr<DolphinConnector> m_dolphinView;
FrameView* m_frameView = nullptr;
QPointer<OverrideView> m_overrideView;
QPointer<SensorView> m_sensorView;
QPointer<DolphinConnector> m_dolphinView;
QPointer<FrameView> m_frameView;
#ifdef USE_FFMPEG
std::unique_ptr<VideoView> m_videoView;
std::unique_ptr<GIFView> m_gifView;
QPointer<VideoView> m_videoView;
QPointer<GIFView> m_gifView;
#endif
#ifdef ENABLE_GDB_STUB

View File

@ -8,39 +8,17 @@
#include "ConfigController.h"
#include "GBAApp.h"
#include "LibraryGrid.h"
#include "LibraryTree.h"
#include "LibraryModel.h"
#include "utils.h"
#include <QHeaderView>
#include <QListView>
#include <QSortFilterProxyModel>
#include <QTimer>
#include <QTreeView>
using namespace QGBA;
LibraryEntry::LibraryEntry(const mLibraryEntry* entry)
: base(entry->base)
, filename(entry->filename)
, fullpath(QString("%1/%2").arg(entry->base, entry->filename))
, title(entry->title)
, internalTitle(entry->internalTitle)
, internalCode(entry->internalCode)
, platform(entry->platform)
, filesize(entry->filesize)
, crc32(entry->crc32)
{
}
void AbstractGameList::addEntry(const LibraryEntry& item) {
addEntries({item});
}
void AbstractGameList::updateEntry(const LibraryEntry& item) {
updateEntries({item});
}
void AbstractGameList::removeEntry(const QString& item) {
removeEntries({item});
}
void AbstractGameList::setShowFilename(bool showFilename) {
m_showFilename = showFilename;
}
LibraryController::LibraryController(QWidget* parent, const QString& path, ConfigController* config)
: QStackedWidget(parent)
, m_config(config)
@ -55,14 +33,47 @@ LibraryController::LibraryController(QWidget* parent, const QString& path, Confi
mLibraryAttachGameDB(m_library.get(), GBAApp::app()->gameDB());
m_libraryTree = std::make_unique<LibraryTree>(this);
addWidget(m_libraryTree->widget());
m_libraryModel = new LibraryModel(this);
m_libraryGrid = std::make_unique<LibraryGrid>(this);
addWidget(m_libraryGrid->widget());
m_treeView = new QTreeView(this);
addWidget(m_treeView);
m_treeModel = new QSortFilterProxyModel(this);
m_treeModel->setSourceModel(m_libraryModel);
m_treeModel->setSortRole(Qt::EditRole);
m_treeView->setModel(m_treeModel);
m_treeView->setSortingEnabled(true);
m_treeView->setAlternatingRowColors(true);
m_currentStyle = LibraryStyle::STYLE_TREE; // Make sure setViewStyle does something
setViewStyle(LibraryStyle::STYLE_LIST);
m_listView = new QListView(this);
addWidget(m_listView);
m_listModel = new QSortFilterProxyModel(this);
m_listModel->setSourceModel(m_libraryModel);
m_listModel->setSortRole(Qt::EditRole);
m_listView->setModel(m_listModel);
QObject::connect(m_treeView, &QAbstractItemView::activated, this, &LibraryController::startGame);
QObject::connect(m_listView, &QAbstractItemView::activated, this, &LibraryController::startGame);
QObject::connect(m_treeView->header(), &QHeaderView::sortIndicatorChanged, this, &LibraryController::sortChanged);
m_expandThrottle.setInterval(100);
m_expandThrottle.setSingleShot(true);
QObject::connect(&m_expandThrottle, &QTimer::timeout, this, qOverload<>(&LibraryController::resizeTreeView));
QObject::connect(m_libraryModel, &QAbstractItemModel::modelReset, &m_expandThrottle, qOverload<>(&QTimer::start));
QObject::connect(m_libraryModel, &QAbstractItemModel::rowsInserted, &m_expandThrottle, qOverload<>(&QTimer::start));
LibraryStyle libraryStyle = LibraryStyle(m_config->getOption("libraryStyle", int(LibraryStyle::STYLE_LIST)).toInt());
updateViewStyle(libraryStyle);
QVariant librarySort = m_config->getQtOption("librarySort");
QVariant librarySortOrder = m_config->getQtOption("librarySortOrder");
if (librarySort.isNull() || !librarySort.canConvert<int>()) {
librarySort = 0;
}
if (librarySortOrder.isNull() || !librarySortOrder.canConvert<Qt::SortOrder>()) {
librarySortOrder = Qt::AscendingOrder;
}
m_treeModel->sort(librarySort.toInt(), librarySortOrder.value<Qt::SortOrder>());
m_listModel->sort(0, Qt::AscendingOrder);
refresh();
}
@ -73,32 +84,63 @@ void LibraryController::setViewStyle(LibraryStyle newStyle) {
if (m_currentStyle == newStyle) {
return;
}
m_currentStyle = newStyle;
updateViewStyle(newStyle);
}
AbstractGameList* newCurrentList = nullptr;
if (newStyle == LibraryStyle::STYLE_LIST || newStyle == LibraryStyle::STYLE_TREE) {
newCurrentList = m_libraryTree.get();
} else {
newCurrentList = m_libraryGrid.get();
void LibraryController::updateViewStyle(LibraryStyle newStyle) {
QString selected;
if (m_currentView) {
QModelIndex selectedIndex = m_currentView->selectionModel()->currentIndex();
if (selectedIndex.isValid()) {
selected = selectedIndex.data(LibraryModel::FullPathRole).toString();
}
}
newCurrentList->selectEntry(selectedEntry().fullpath);
newCurrentList->setViewStyle(newStyle);
setCurrentWidget(newCurrentList->widget());
m_currentList = newCurrentList;
m_currentStyle = newStyle;
m_libraryModel->setTreeMode(newStyle == LibraryStyle::STYLE_TREE);
QAbstractItemView* newView = m_listView;
if (newStyle == LibraryStyle::STYLE_LIST || newStyle == LibraryStyle::STYLE_TREE) {
newView = m_treeView;
}
setCurrentWidget(newView);
m_currentView = newView;
selectEntry(selected);
}
void LibraryController::sortChanged(int column, Qt::SortOrder order) {
m_config->setQtOption("librarySort", column);
m_config->setQtOption("librarySortOrder", order);
}
void LibraryController::selectEntry(const QString& fullpath) {
if (!m_currentList) {
if (!m_currentView) {
return;
}
m_currentList->selectEntry(fullpath);
QModelIndex index = m_libraryModel->index(fullpath);
// If the model is proxied in the current view, map the index to the proxy
QAbstractProxyModel* proxy = qobject_cast<QAbstractProxyModel*>(m_currentView->model());
if (proxy) {
index = proxy->mapFromSource(index);
}
if (index.isValid()) {
m_currentView->selectionModel()->setCurrentIndex(index, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Current);
}
}
LibraryEntry LibraryController::selectedEntry() {
if (!m_currentList) {
if (!m_currentView) {
return {};
}
return m_entries.value(m_currentList->selectedEntry());
QModelIndex index = m_currentView->selectionModel()->currentIndex();
if (!index.isValid()) {
return {};
}
QString fullpath = index.data(LibraryModel::FullPathRole).toString();
return m_libraryModel->entry(fullpath);
}
VFile* LibraryController::selectedVFile() {
@ -110,6 +152,7 @@ VFile* LibraryController::selectedVFile() {
libentry.base = baseUtf8.constData();
libentry.filename = filenameUtf8.constData();
libentry.platform = mPLATFORM_NONE;
libentry.platformModels = M_LIBRARY_MODEL_UNKNOWN;
return mLibraryOpenVFile(m_library.get(), &libentry);
} else {
return nullptr;
@ -149,42 +192,34 @@ void LibraryController::refresh() {
setDisabled(true);
QHash<QString, LibraryEntry> removedEntries = m_entries;
QHash<QString, LibraryEntry> updatedEntries;
QSet<QString> removedEntries(qListToSet(m_knownGames.keys()));
QList<LibraryEntry> updatedEntries;
QList<LibraryEntry> newEntries;
mLibraryListing listing;
mLibraryListingInit(&listing, 0);
mLibraryGetEntries(m_library.get(), &listing, 0, 0, nullptr);
for (size_t i = 0; i < mLibraryListingSize(&listing); i++) {
LibraryEntry entry = mLibraryListingGetConstPointer(&listing, i);
if (!m_entries.contains(entry.fullpath)) {
const mLibraryEntry* entry = mLibraryListingGetConstPointer(&listing, i);
uint64_t checkHash = LibraryEntry::checkHash(entry);
QString fullpath = QStringLiteral("%1/%2").arg(entry->base, entry->filename);
if (!m_knownGames.contains(fullpath)) {
newEntries.append(entry);
} else {
updatedEntries[entry.fullpath] = entry;
} else if (checkHash != m_knownGames[fullpath]) {
updatedEntries.append(entry);
}
m_entries[entry.fullpath] = entry;
removedEntries.remove(entry.fullpath);
removedEntries.remove(fullpath);
m_knownGames[fullpath] = checkHash;
}
// Check for entries that were removed
for (QString& path : removedEntries.keys()) {
m_entries.remove(path);
for (const QString& path : removedEntries) {
m_knownGames.remove(path);
}
if (!removedEntries.size() && !newEntries.size()) {
m_libraryTree->updateEntries(updatedEntries.values());
m_libraryGrid->updateEntries(updatedEntries.values());
} else if (!updatedEntries.size()) {
m_libraryTree->removeEntries(removedEntries.keys());
m_libraryGrid->removeEntries(removedEntries.keys());
m_libraryTree->addEntries(newEntries);
m_libraryGrid->addEntries(newEntries);
} else {
m_libraryTree->resetEntries(m_entries.values());
m_libraryGrid->resetEntries(m_entries.values());
}
m_libraryModel->removeEntries(removedEntries.values());
m_libraryModel->updateEntries(updatedEntries);
m_libraryModel->addEntries(newEntries);
for (size_t i = 0; i < mLibraryListingSize(&listing); ++i) {
mLibraryEntryFree(mLibraryListingGetPointer(&listing, i));
@ -201,7 +236,7 @@ void LibraryController::selectLastBootedGame() {
return;
}
const QString lastfile = m_config->getMRU().first();
if (m_entries.contains(lastfile)) {
if (m_knownGames.contains(lastfile)) {
selectEntry(lastfile);
}
}
@ -213,16 +248,61 @@ void LibraryController::loadDirectory(const QString& dir, bool recursive) {
mLibraryLoadDirectory(library.get(), dir.toUtf8().constData(), recursive);
m_libraryJob.testAndSetOrdered(libraryJob, -1);
}
void LibraryController::setShowFilename(bool showFilename) {
if (showFilename == m_showFilename) {
return;
}
m_showFilename = showFilename;
if (m_libraryGrid) {
m_libraryGrid->setShowFilename(m_showFilename);
}
if (m_libraryTree) {
m_libraryTree->setShowFilename(m_showFilename);
}
m_libraryModel->setShowFilename(m_showFilename);
refresh();
}
void LibraryController::showEvent(QShowEvent*) {
resizeTreeView(false);
}
void LibraryController::resizeEvent(QResizeEvent*) {
resizeTreeView(false);
}
// This function automatically reallocates the horizontal space between the
// columns in the view in a useful way when the window is resized.
void LibraryController::resizeTreeView(bool expand) {
// When new items are added to the model, make sure they are revealed.
if (expand) {
m_treeView->expandAll();
}
// Start off by asking the view how wide it thinks each column should be.
int viewportWidth = m_treeView->viewport()->width();
int totalWidth = m_treeView->header()->sectionSizeHint(LibraryModel::MAX_COLUMN);
for (int column = 0; column < LibraryModel::MAX_COLUMN; column++) {
totalWidth += m_treeView->columnWidth(column);
}
// If there would be empty space, ask the view to redistribute it.
// The final column is set to fill any remaining width, so this
// should (at least) fill the window.
if (totalWidth < viewportWidth) {
totalWidth = 0;
for (int column = 0; column <= LibraryModel::MAX_COLUMN; column++) {
m_treeView->resizeColumnToContents(column);
totalWidth += m_treeView->columnWidth(column);
}
}
// If the columns would be too wide for the view now, try shrinking the
// "Location" column down to reduce horizontal scrolling, with a fixed
// minimum width of 100px.
if (totalWidth > viewportWidth) {
int locationWidth = m_treeView->columnWidth(LibraryModel::COL_LOCATION);
if (locationWidth > 100) {
int newLocationWidth = m_treeView->viewport()->width() - (totalWidth - locationWidth);
if (newLocationWidth < 100) {
newLocationWidth = 100;
}
m_treeView->setColumnWidth(LibraryModel::COL_LOCATION, newLocationWidth);
}
}
}

View File

@ -12,15 +12,21 @@
#include <QHash>
#include <QList>
#include <QStackedWidget>
#include <QTimer>
#include <mgba/core/library.h>
#include "LibraryEntry.h"
class QAbstractItemView;
class QListView;
class QSortFilterProxyModel;
class QTreeView;
namespace QGBA {
// Predefinitions
class LibraryGrid;
class LibraryTree;
class ConfigController;
class LibraryModel;
enum class LibraryStyle {
STYLE_LIST = 0,
@ -29,50 +35,6 @@ enum class LibraryStyle {
STYLE_ICON
};
struct LibraryEntry {
LibraryEntry() {}
LibraryEntry(const mLibraryEntry* entry);
bool isNull() const { return fullpath.isNull(); }
QString displayTitle() const { return title.isNull() ? filename : title; }
QString base;
QString filename;
QString fullpath;
QString title;
QByteArray internalTitle;
QByteArray internalCode;
mPlatform platform;
size_t filesize;
uint32_t crc32;
bool operator==(const LibraryEntry& other) const { return other.fullpath == fullpath; }
};
class AbstractGameList {
public:
virtual QString selectedEntry() = 0;
virtual void selectEntry(const QString& fullpath) = 0;
virtual void setViewStyle(LibraryStyle newStyle) = 0;
virtual void resetEntries(const QList<LibraryEntry>&) = 0;
virtual void addEntries(const QList<LibraryEntry>&) = 0;
virtual void updateEntries(const QList<LibraryEntry>&) = 0;
virtual void removeEntries(const QList<QString>&) = 0;
virtual void addEntry(const LibraryEntry&);
virtual void updateEntry(const LibraryEntry&);
virtual void removeEntry(const QString&);
virtual void setShowFilename(bool showFilename);
virtual QWidget* widget() = 0;
protected:
bool m_showFilename = false;
};
class LibraryController final : public QStackedWidget {
Q_OBJECT
@ -103,21 +65,34 @@ signals:
private slots:
void refresh();
void sortChanged(int column, Qt::SortOrder order);
inline void resizeTreeView() { resizeTreeView(true); }
void resizeTreeView(bool expand);
protected:
void showEvent(QShowEvent*) override;
void resizeEvent(QResizeEvent*) override;
private:
void loadDirectory(const QString&, bool recursive = true); // Called on separate thread
void updateViewStyle(LibraryStyle newStyle);
ConfigController* m_config = nullptr;
std::shared_ptr<mLibrary> m_library;
QAtomicInteger<qint64> m_libraryJob = -1;
QHash<QString, LibraryEntry> m_entries;
LibraryStyle m_currentStyle;
AbstractGameList* m_currentList = nullptr;
std::unique_ptr<LibraryGrid> m_libraryGrid;
std::unique_ptr<LibraryTree> m_libraryTree;
QHash<QString, uint64_t> m_knownGames;
LibraryModel* m_libraryModel;
QSortFilterProxyModel* m_listModel;
QSortFilterProxyModel* m_treeModel;
QListView* m_listView;
QTreeView* m_treeView;
QAbstractItemView* m_currentView = nullptr;
bool m_showFilename = false;
QTimer m_expandThrottle;
};
}

View File

@ -0,0 +1,73 @@
/* Copyright (c) 2014-2017 waddlesplash
* Copyright (c) 2013-2021 Jeffrey Pfau
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "LibraryEntry.h"
#include "utils.h"
#include <mgba/core/library.h>
using namespace QGBA;
static inline uint64_t getSha1Prefix(const uint8_t* sha1) {
return *reinterpret_cast<const quint64*>(sha1);
}
static inline uint64_t getSha1Prefix(const QByteArray& sha1) {
if (sha1.size() < 8) {
return 0;
}
return getSha1Prefix((const uint8_t*)sha1.constData());
}
static inline uint64_t checkHash(size_t filesize, uint32_t crc32, uint64_t sha1Prefix) {
if (sha1Prefix) {
return sha1Prefix;
}
return (uint64_t(filesize) << 32) ^ ((crc32 + 1ULL) * (uint32_t(filesize) + 1ULL));
}
LibraryEntry::LibraryEntry(const mLibraryEntry* entry)
: base(entry->base)
, filename(entry->filename)
, fullpath(QString("%1/%2").arg(entry->base, entry->filename))
, title(entry->title)
, internalTitle(entry->internalTitle)
, internalCode(entry->internalCode)
, platform(entry->platform)
, platformModels(entry->platformModels)
, filesize(entry->filesize)
, crc32(entry->crc32)
, sha1(reinterpret_cast<const char*>(entry->sha1), sizeof(entry->sha1))
{
}
bool LibraryEntry::isNull() const {
return fullpath.isNull();
}
QString LibraryEntry::displayTitle(bool showFilename) const {
if (showFilename || title.isNull()) {
return filename;
}
return title;
}
QString LibraryEntry::displayPlatform() const {
return nicePlatformFormat(platform, platformModels);
}
bool LibraryEntry::operator==(const LibraryEntry& other) const {
return other.fullpath == fullpath;
}
uint64_t LibraryEntry::checkHash() const {
return ::checkHash(filesize, crc32, getSha1Prefix(sha1));
}
uint64_t LibraryEntry::checkHash(const mLibraryEntry* entry) {
return ::checkHash(entry->filesize, entry->crc32, getSha1Prefix(entry->sha1));
}

View File

@ -0,0 +1,50 @@
/* Copyright (c) 2014-2017 waddlesplash
* Copyright (c) 2013-2021 Jeffrey Pfau
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#pragma once
#include <QByteArray>
#include <QList>
#include <QString>
#include <mgba/core/core.h>
struct mLibraryEntry;
namespace QGBA {
struct LibraryEntry {
LibraryEntry() = default;
LibraryEntry(const LibraryEntry&) = default;
LibraryEntry(LibraryEntry&&) = default;
LibraryEntry(const mLibraryEntry* entry);
bool isNull() const;
QString displayTitle(bool showFilename = false) const;
QString displayPlatform() const;
QString base;
QString filename;
QString fullpath;
QString title;
QByteArray internalTitle;
QByteArray internalCode;
mPlatform platform;
int platformModels;
size_t filesize;
uint32_t crc32;
QByteArray sha1;
LibraryEntry& operator=(const LibraryEntry&) = default;
LibraryEntry& operator=(LibraryEntry&&) = default;
bool operator==(const LibraryEntry& other) const;
uint64_t checkHash() const;
static uint64_t checkHash(const mLibraryEntry* entry);
};
};

View File

@ -0,0 +1,473 @@
/* Copyright (c) 2013-2022 Jeffrey Pfau
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "LibraryModel.h"
#include "../utils.h"
#include <QApplication>
#include <QDir>
#include <QItemSelectionModel>
#include <QSortFilterProxyModel>
#include <QStyle>
#include <algorithm>
using namespace QGBA;
static const QStringList iconSets{
"GBA",
"GBC",
"GB",
"SGB",
};
static QHash<QString, QIcon> platformIcons;
LibraryModel::LibraryModel(QObject* parent)
: QAbstractItemModel(parent)
, m_treeMode(false)
, m_showFilename(false)
{
if (platformIcons.isEmpty()) {
for (const QString& platform : iconSets) {
QString pathTemplate = QStringLiteral(":/res/%1-icon%2").arg(platform.toLower());
QIcon icon;
icon.addFile(pathTemplate.arg("-256.png"), QSize(256, 256));
icon.addFile(pathTemplate.arg("-128.png"), QSize(128, 128));
icon.addFile(pathTemplate.arg("-32.png"), QSize(32, 32));
icon.addFile(pathTemplate.arg("-24.png"), QSize(24, 24));
icon.addFile(pathTemplate.arg("-16.png"), QSize(16, 16));
// This will silently and harmlessly fail if QSvgIconEngine isn't compiled in.
icon.addFile(pathTemplate.arg(".svg"));
platformIcons[platform] = icon;
}
}
}
bool LibraryModel::treeMode() const {
return m_treeMode;
}
void LibraryModel::setTreeMode(bool tree) {
if (m_treeMode == tree) {
return;
}
beginResetModel();
m_treeMode = tree;
endResetModel();
}
bool LibraryModel::showFilename() const {
return m_showFilename;
}
void LibraryModel::setShowFilename(bool show) {
if (m_showFilename == show) {
return;
}
m_showFilename = show;
if (m_treeMode) {
int numPaths = m_pathOrder.size();
for (int i = 0; i < numPaths; i++) {
QModelIndex parent = index(i, 0);
emit dataChanged(index(0, 0, parent), index(m_pathIndex[m_pathOrder[i]].size() - 1, 0));
}
} else {
emit dataChanged(index(0, 0), index(rowCount() - 1, 0));
}
}
void LibraryModel::resetEntries(const QList<LibraryEntry>& items) {
beginResetModel();
blockSignals(true);
m_games.clear();
m_pathOrder.clear();
m_pathIndex.clear();
addEntriesList(items);
blockSignals(false);
endResetModel();
}
void LibraryModel::addEntries(const QList<LibraryEntry>& items) {
if (items.isEmpty()) {
return;
} else if (m_treeMode) {
addEntriesTree(items);
} else {
addEntriesList(items);
}
}
void LibraryModel::addEntryInternal(const LibraryEntry& item) {
m_gameIndex[item.fullpath] = m_games.size();
m_games.emplace_back(new LibraryEntry(item));
if (!m_pathIndex.contains(item.base)) {
m_pathOrder << item.base;
}
m_pathIndex[item.base] << m_games.back().get();
}
void LibraryModel::addEntriesList(const QList<LibraryEntry>& items) {
beginInsertRows(QModelIndex(), m_games.size(), m_games.size() + items.size() - 1);
for (const LibraryEntry& item : items) {
addEntryInternal(item);
}
endInsertRows();
}
void LibraryModel::addEntriesTree(const QList<LibraryEntry>& items) {
QHash<QString, QList<const LibraryEntry*>> byPath;
QHash<QString, QList<const LibraryEntry*>> newPaths;
for (const LibraryEntry& item : items) {
if (m_pathIndex.contains(item.base)) {
byPath[item.base] << &item;
} else {
newPaths[item.base] << &item;
}
}
if (newPaths.size() > 0) {
beginInsertRows(QModelIndex(), m_pathIndex.size(), m_pathIndex.size() + newPaths.size() - 1);
for (const QString& base : newPaths.keys()) {
for (const LibraryEntry* item : newPaths[base]) {
addEntryInternal(*item);
}
}
endInsertRows();
}
for (const QString& base : byPath.keys()) {
QList<const LibraryEntry*>& pathItems = m_pathIndex[base];
QList<const LibraryEntry*>& newItems = byPath[base];
QModelIndex parent = indexForPath(base);
beginInsertRows(parent, pathItems.size(), pathItems.size() + newItems.size() - 1);
for (const LibraryEntry* item : newItems) {
addEntryInternal(*item);
}
endInsertRows();
}
}
void LibraryModel::updateEntries(const QList<LibraryEntry>& items) {
QHash<QModelIndex, SpanSet> updatedSpans;
for (const LibraryEntry& item : items) {
QModelIndex idx = index(item.fullpath);
Q_ASSERT(idx.isValid());
int pos = m_gameIndex.value(item.fullpath, -1);
Q_ASSERT(pos >= 0);
*m_games[pos] = item;
updatedSpans[idx.parent()].add(pos);
}
for (auto iter = updatedSpans.begin(); iter != updatedSpans.end(); iter++) {
QModelIndex parent = iter.key();
SpanSet spans = iter.value();
spans.merge();
for (const SpanSet::Span& span : spans.spans) {
QModelIndex topLeft = index(span.left, 0, parent);
QModelIndex bottomRight = index(span.right, MAX_COLUMN, parent);
emit dataChanged(topLeft, bottomRight);
}
}
}
void LibraryModel::removeEntries(const QList<QString>& items) {
SpanSet removedRootSpans, removedGameSpans;
QHash<QString, SpanSet> removedTreeSpans;
int firstModifiedIndex = m_games.size();
// Remove the items from the game index and assemble a span
// set so that we can later inform the view of which rows
// were removed in an optimized way.
for (const QString& item : items) {
int pos = m_gameIndex.value(item, -1);
Q_ASSERT(pos >= 0);
if (pos < firstModifiedIndex) {
firstModifiedIndex = pos;
}
LibraryEntry* entry = m_games[pos].get();
QModelIndex parent = indexForPath(entry->base);
Q_ASSERT(!m_treeMode || parent.isValid());
QList<const LibraryEntry*>& pathItems = m_pathIndex[entry->base];
int pathPos = pathItems.indexOf(entry);
Q_ASSERT(pathPos >= 0);
removedGameSpans.add(pos);
removedTreeSpans[entry->base].add(pathPos);
m_gameIndex.remove(item);
}
if (!m_treeMode) {
// If not using a tree view, all entries are root entries.
removedRootSpans = removedGameSpans;
}
// Remove the paths from the path indexes.
// If it's a tree view, inform the view.
for (const QString& base : removedTreeSpans.keys()) {
SpanSet& spanSet = removedTreeSpans[base];
spanSet.merge();
QList<const LibraryEntry*>& pathIndex = m_pathIndex[base];
if (spanSet.spans.size() == 1) {
SpanSet::Span span = spanSet.spans[0];
if (span.left == 0 && span.right == pathIndex.size() - 1) {
if (m_treeMode) {
removedRootSpans.add(m_pathOrder.indexOf(base));
} else {
m_pathIndex.remove(base);
m_pathOrder.removeAll(base);
}
continue;
}
}
QModelIndex parent = indexForPath(base);
spanSet.sort(true);
for (const SpanSet::Span& span : spanSet.spans) {
if (m_treeMode) {
beginRemoveRows(parent, span.left, span.right);
}
pathIndex.erase(pathIndex.begin() + span.left, pathIndex.begin() + span.right + 1);
if (m_treeMode) {
endRemoveRows();
}
}
}
// Remove the games from the backing store and path indexes,
// and tell the view to remove the root items.
removedRootSpans.merge();
removedRootSpans.sort(true);
for (const SpanSet::Span& span : removedRootSpans.spans) {
beginRemoveRows(QModelIndex(), span.left, span.right);
if (m_treeMode) {
for (int i = span.right; i >= span.left; i--) {
QString base = m_pathOrder.takeAt(i);
m_pathIndex.remove(base);
}
} else {
// In list view, remove games from the backing store immediately
m_games.erase(m_games.begin() + span.left, m_games.begin() + span.right + 1);
}
endRemoveRows();
}
if (m_treeMode) {
// In tree view, remove them after cleaning up the path indexes.
removedGameSpans.merge();
removedGameSpans.sort(true);
for (const SpanSet::Span& span : removedGameSpans.spans) {
m_games.erase(m_games.begin() + span.left, m_games.begin() + span.right + 1);
}
}
// Finally, update the game index for the remaining items.
for (int i = m_games.size() - 1; i >= firstModifiedIndex; i--) {
m_gameIndex[m_games[i]->fullpath] = i;
}
}
QModelIndex LibraryModel::index(const QString& game) const {
int pos = m_gameIndex.value(game, -1);
if (pos < 0) {
return QModelIndex();
}
if (m_treeMode) {
const LibraryEntry& entry = *m_games[pos];
return createIndex(m_pathIndex[entry.base].indexOf(&entry), 0, m_pathOrder.indexOf(entry.base));
}
return createIndex(pos, 0);
}
QModelIndex LibraryModel::index(int row, int column, const QModelIndex& parent) const {
if (!parent.isValid()) {
return createIndex(row, column, quintptr(0));
}
if (!m_treeMode || parent.internalId() || parent.column() != 0) {
return QModelIndex();
}
return createIndex(row, column, parent.row() + 1);
}
QModelIndex LibraryModel::parent(const QModelIndex& child) const {
if (!child.isValid() || child.internalId() == 0) {
return QModelIndex();
}
return createIndex(child.internalId() - 1, 0, quintptr(0));
}
int LibraryModel::columnCount(const QModelIndex& parent) const {
if (!parent.isValid() || (parent.column() == 0 && !parent.parent().isValid())) {
return MAX_COLUMN + 1;
}
return 0;
}
int LibraryModel::rowCount(const QModelIndex& parent) const {
if (parent.isValid()) {
if (m_treeMode) {
if (parent.row() < 0 || parent.row() >= m_pathOrder.size() || parent.column() != 0) {
return 0;
}
return m_pathIndex[m_pathOrder[parent.row()]].size();
}
return 0;
}
if (m_treeMode) {
return m_pathOrder.size();
}
return m_games.size();
}
QVariant LibraryModel::folderData(const QModelIndex& index, int role) const {
// Precondition: index and role must have already been validated
if (role == Qt::DecorationRole) {
return qApp->style()->standardIcon(QStyle::SP_DirOpenIcon);
}
if (role == FullPathRole || (index.column() == COL_LOCATION && role != Qt::DisplayRole)) {
return m_pathOrder[index.row()];
}
if (index.column() == COL_NAME) {
QString path = m_pathOrder[index.row()];
return path.section('/', -1);
}
return QVariant();
}
bool LibraryModel::validateIndex(const QModelIndex& index) const
{
if (index.model() != this || index.row() < 0 || index.column() < 0 || index.column() > MAX_COLUMN) {
// Obviously invalid index
return false;
}
if (index.parent().isValid() && !validateIndex(index.parent())) {
// Parent index is invalid
return false;
}
if (index.row() >= rowCount(index.parent())) {
// Row is out of bounds for this level of hierarchy
return false;
}
return true;
}
QVariant LibraryModel::data(const QModelIndex& index, int role) const {
switch (role) {
case Qt::DisplayRole:
case Qt::EditRole:
case Qt::TextAlignmentRole:
case FullPathRole:
break;
case Qt::ToolTipRole:
if (index.column() > COL_LOCATION) {
return QVariant();
}
break;
case Qt::DecorationRole:
if (index.column() != COL_NAME) {
return QVariant();
}
break;
default:
return QVariant();
}
if (!validateIndex(index)) {
return QVariant();
}
if (role == Qt::TextAlignmentRole) {
return index.column() == COL_SIZE ? (int)(Qt::AlignTrailing | Qt::AlignVCenter) : (int)(Qt::AlignLeading | Qt::AlignVCenter);
}
const LibraryEntry* entry = nullptr;
if (m_treeMode) {
if (!index.parent().isValid()) {
return folderData(index, role);
}
QString path = m_pathOrder[index.parent().row()];
entry = m_pathIndex[path][index.row()];
} else if (!index.parent().isValid() && index.row() < (int)m_games.size()) {
entry = m_games[index.row()].get();
}
if (entry) {
if (role == FullPathRole) {
return entry->fullpath;
}
switch (index.column()) {
case COL_NAME:
if (role == Qt::DecorationRole) {
return platformIcons.value(entry->displayPlatform(), qApp->style()->standardIcon(QStyle::SP_FileIcon));
}
return entry->displayTitle(m_showFilename);
case COL_LOCATION:
return QDir::toNativeSeparators(entry->base);
case COL_PLATFORM:
return nicePlatformFormat(entry->platform);
case COL_SIZE:
return (role == Qt::DisplayRole) ? QVariant(niceSizeFormat(entry->filesize)) : QVariant(int(entry->filesize));
case COL_CRC32:
return (role == Qt::DisplayRole) ? QVariant(QStringLiteral("%0").arg(entry->crc32, 8, 16, QChar('0'))) : QVariant(entry->crc32);
}
}
return QVariant();
}
QVariant LibraryModel::headerData(int section, Qt::Orientation orientation, int role) const {
if (orientation == Qt::Horizontal && role == Qt::DisplayRole) {
switch (section) {
case COL_NAME:
return QApplication::translate("LibraryTree", "Name", nullptr);
case COL_LOCATION:
return QApplication::translate("LibraryTree", "Location", nullptr);
case COL_PLATFORM:
return QApplication::translate("LibraryTree", "Platform", nullptr);
case COL_SIZE:
return QApplication::translate("LibraryTree", "Size", nullptr);
case COL_CRC32:
return QApplication::translate("LibraryTree", "CRC32", nullptr);
};
}
return QVariant();
}
QModelIndex LibraryModel::indexForPath(const QString& path) {
int pos = m_pathOrder.indexOf(path);
if (pos < 0) {
pos = m_pathOrder.size();
beginInsertRows(QModelIndex(), pos, pos);
m_pathOrder << path;
m_pathIndex[path] = QList<const LibraryEntry*>();
endInsertRows();
}
if (!m_treeMode) {
return QModelIndex();
}
return index(pos, 0, QModelIndex());
}
QModelIndex LibraryModel::indexForPath(const QString& path) const {
if (!m_treeMode) {
return QModelIndex();
}
int pos = m_pathOrder.indexOf(path);
if (pos < 0) {
return QModelIndex();
}
return index(pos, 0, QModelIndex());
}
LibraryEntry LibraryModel::entry(const QString& game) const {
int pos = m_gameIndex.value(game, -1);
if (pos < 0) {
return {};
}
return *m_games[pos];
}

View File

@ -0,0 +1,88 @@
/* Copyright (c) 2013-2022 Jeffrey Pfau
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#pragma once
#include <QAbstractItemModel>
#include <QIcon>
#include <QTreeView>
#include <mgba/core/library.h>
#include <memory>
#include <vector>
#include "LibraryEntry.h"
class QTreeView;
class LibraryModelTest;
namespace QGBA {
class LibraryModel final : public QAbstractItemModel {
Q_OBJECT
public:
enum Columns {
COL_NAME = 0,
COL_LOCATION = 1,
COL_PLATFORM = 2,
COL_SIZE = 3,
COL_CRC32 = 4,
MAX_COLUMN = 4,
};
enum ItemDataRole {
FullPathRole = Qt::UserRole + 1,
};
explicit LibraryModel(QObject* parent = nullptr);
bool treeMode() const;
void setTreeMode(bool tree);
bool showFilename() const;
void setShowFilename(bool show);
void resetEntries(const QList<LibraryEntry>& items);
void addEntries(const QList<LibraryEntry>& items);
void updateEntries(const QList<LibraryEntry>& items);
void removeEntries(const QList<QString>& items);
QModelIndex index(const QString& game) const;
QModelIndex index(int row, int column, const QModelIndex& parent = QModelIndex()) const override;
QModelIndex parent(const QModelIndex& child) const override;
int columnCount(const QModelIndex& parent = QModelIndex()) const override;
int rowCount(const QModelIndex& parent = QModelIndex()) const override;
QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
LibraryEntry entry(const QString& game) const;
private:
friend class ::LibraryModelTest;
QModelIndex indexForPath(const QString& path);
QModelIndex indexForPath(const QString& path) const;
QVariant folderData(const QModelIndex& index, int role = Qt::DisplayRole) const;
bool validateIndex(const QModelIndex& index) const;
void addEntriesList(const QList<LibraryEntry>& items);
void addEntriesTree(const QList<LibraryEntry>& items);
void addEntryInternal(const LibraryEntry& item);
bool m_treeMode;
bool m_showFilename;
std::vector<std::unique_ptr<LibraryEntry>> m_games;
QStringList m_pathOrder;
QHash<QString, QList<const LibraryEntry*>> m_pathIndex;
QHash<QString, int> m_gameIndex;
};
}

View File

@ -1,212 +0,0 @@
/* Copyright (c) 2014-2017 waddlesplash
* Copyright (c) 2013-2022 Jeffrey Pfau
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "LibraryTree.h"
#include "utils.h"
#include <QApplication>
#include <QDir>
using namespace QGBA;
namespace QGBA {
class LibraryTreeItem : public QTreeWidgetItem {
public:
LibraryTreeItem(QTreeWidget* parent = nullptr) : QTreeWidgetItem(parent) {}
void setFilesize(size_t size);
virtual bool operator<(const QTreeWidgetItem& other) const override;
protected:
size_t m_size = 0;
};
}
void LibraryTreeItem::setFilesize(size_t size) {
m_size = size;
setText(LibraryTree::COL_SIZE, niceSizeFormat(size));
}
bool LibraryTreeItem::operator<(const QTreeWidgetItem& other) const {
const int column = treeWidget()->sortColumn();
return ((column == LibraryTree::COL_SIZE) ?
m_size < dynamic_cast<const LibraryTreeItem*>(&other)->m_size :
QTreeWidgetItem::operator<(other));
}
LibraryTree::LibraryTree(LibraryController* parent)
: m_widget(new QTreeWidget(parent))
, m_controller(parent)
{
m_widget->setObjectName("LibraryTree");
m_widget->setSortingEnabled(true);
m_widget->setAlternatingRowColors(true);
QTreeWidgetItem* header = new QTreeWidgetItem({
QApplication::translate("QGBA::LibraryTree", "Name", nullptr),
QApplication::translate("QGBA::LibraryTree", "Location", nullptr),
QApplication::translate("QGBA::LibraryTree", "Platform", nullptr),
QApplication::translate("QGBA::LibraryTree", "Size", nullptr),
QApplication::translate("QGBA::LibraryTree", "CRC32", nullptr),
});
header->setTextAlignment(3, Qt::AlignTrailing | Qt::AlignVCenter);
m_widget->setHeaderItem(header);
setViewStyle(LibraryStyle::STYLE_TREE);
m_widget->sortByColumn(COL_NAME, Qt::AscendingOrder);
QObject::connect(m_widget, &QTreeWidget::itemActivated, [this](QTreeWidgetItem* item, int) -> void {
if (m_items.values().contains(item)) {
emit m_controller->startGame();
}
});
}
LibraryTree::~LibraryTree() {
m_widget->clear();
}
void LibraryTree::resizeAllCols() {
for (int i = 0; i < m_widget->columnCount(); i++) {
m_widget->resizeColumnToContents(i);
}
}
QString LibraryTree::selectedEntry() {
if (!m_widget->selectedItems().empty()) {
return m_items.key(m_widget->selectedItems().at(0));
} else {
return {};
}
}
void LibraryTree::selectEntry(const QString& game) {
if (game.isNull()) {
return;
}
m_widget->setCurrentItem(m_items.value(game));
}
void LibraryTree::setViewStyle(LibraryStyle newStyle) {
if (newStyle == LibraryStyle::STYLE_LIST) {
m_widget->setIndentation(0);
} else {
m_widget->setIndentation(20);
}
m_currentStyle = newStyle;
rebuildTree();
}
void LibraryTree::resetEntries(const QList<LibraryEntry>& items) {
m_deferredTreeRebuild = true;
m_entries.clear();
m_pathNodes.clear();
addEntries(items);
}
void LibraryTree::addEntries(const QList<LibraryEntry>& items) {
m_deferredTreeRebuild = true;
for (const auto& item : items) {
addEntry(item);
}
m_deferredTreeRebuild = false;
rebuildTree();
}
void LibraryTree::addEntry(const LibraryEntry& item) {
m_entries[item.fullpath] = item;
QString folder = item.base;
if (!m_pathNodes.contains(folder)) {
m_pathNodes.insert(folder, 1);
} else {
++m_pathNodes[folder];
}
rebuildTree();
}
void LibraryTree::updateEntries(const QList<LibraryEntry>& items) {
for (const auto& item : items) {
updateEntry(item);
}
}
void LibraryTree::updateEntry(const LibraryEntry& item) {
m_entries[item.fullpath] = item;
LibraryTreeItem* i = static_cast<LibraryTreeItem*>(m_items.value(item.fullpath));
i->setText(COL_NAME, m_showFilename ? item.filename : item.displayTitle());
i->setText(COL_PLATFORM, nicePlatformFormat(item.platform));
i->setFilesize(item.filesize);
i->setText(COL_CRC32, QString("%0").arg(item.crc32, 8, 16, QChar('0')));
}
void LibraryTree::removeEntries(const QList<QString>& items) {
m_deferredTreeRebuild = true;
for (const auto& item : items) {
removeEntry(item);
}
m_deferredTreeRebuild = false;
rebuildTree();
}
void LibraryTree::removeEntry(const QString& item) {
if (!m_entries.contains(item)) {
return;
}
QString folder = m_entries.value(item).base;
--m_pathNodes[folder];
if (m_pathNodes[folder] <= 0) {
m_pathNodes.remove(folder);
}
m_entries.remove(item);
rebuildTree();
}
void LibraryTree::rebuildTree() {
if (m_deferredTreeRebuild) {
return;
}
QString currentGame = selectedEntry();
m_widget->clear();
m_items.clear();
QHash<QString, QTreeWidgetItem*> pathNodes;
if (m_currentStyle == LibraryStyle::STYLE_TREE) {
for (const QString& folder : m_pathNodes.keys()) {
QTreeWidgetItem* i = new LibraryTreeItem;
pathNodes.insert(folder, i);
i->setText(0, folder.section("/", -1));
m_widget->addTopLevelItem(i);
}
}
for (const auto& item : m_entries.values()) {
LibraryTreeItem* i = new LibraryTreeItem;
i->setText(COL_NAME, item.displayTitle());
i->setText(COL_LOCATION, QDir::toNativeSeparators(item.base));
i->setText(COL_PLATFORM, nicePlatformFormat(item.platform));
i->setFilesize(item.filesize);
i->setTextAlignment(COL_SIZE, Qt::AlignTrailing | Qt::AlignVCenter);
i->setText(COL_CRC32, QString("%0").arg(item.crc32, 8, 16, QChar('0')));
m_items.insert(item.fullpath, i);
if (m_currentStyle == LibraryStyle::STYLE_TREE) {
pathNodes.value(item.base)->addChild(i);
} else {
m_widget->addTopLevelItem(i);
}
}
m_widget->expandAll();
resizeAllCols();
selectEntry(currentGame);
}

View File

@ -1,61 +0,0 @@
/* Copyright (c) 2014-2017 waddlesplash
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#pragma once
#include <QTreeWidget>
#include "LibraryController.h"
namespace QGBA {
class LibraryTreeItem;
class LibraryTree final : public AbstractGameList {
public:
enum Columns {
COL_NAME = 0,
COL_LOCATION = 1,
COL_PLATFORM = 2,
COL_SIZE = 3,
COL_CRC32 = 4,
};
explicit LibraryTree(LibraryController* parent = nullptr);
~LibraryTree();
QString selectedEntry() override;
void selectEntry(const QString& fullpath) override;
void setViewStyle(LibraryStyle newStyle) override;
void resetEntries(const QList<LibraryEntry>& items) override;
void addEntries(const QList<LibraryEntry>& items) override;
void updateEntries(const QList<LibraryEntry>& items) override;
void removeEntries(const QList<QString>& items) override;
void addEntry(const LibraryEntry& items) override;
void updateEntry(const LibraryEntry& items) override;
void removeEntry(const QString& items) override;
QWidget* widget() override { return m_widget; }
private:
QTreeWidget* m_widget;
LibraryStyle m_currentStyle;
LibraryController* m_controller;
bool m_deferredTreeRebuild = false;
QHash<QString, LibraryEntry> m_entries;
QHash<QString, QTreeWidgetItem*> m_items;
QHash<QString, int> m_pathNodes;
void rebuildTree();
void resizeAllCols();
};
}

View File

@ -50,6 +50,7 @@ Q_IMPORT_PLUGIN(QWaylandIntegrationPlugin);
#ifdef Q_OS_WIN
#include <process.h>
#include <wincon.h>
extern "C" __declspec (dllexport) DWORD NoHotPatch = 0x1;
#else
#include <unistd.h>
#endif

View File

@ -7,6 +7,30 @@
<file>../../../res/keymap.qpic</file>
<file>../../../res/patrons.txt</file>
<file>../../../res/no-cam.png</file>
<file>../../../res/gb-icon-256.png</file>
<file>../../../res/gb-icon-128.png</file>
<file>../../../res/gb-icon-32.png</file>
<file>../../../res/gb-icon-24.png</file>
<file>../../../res/gb-icon-16.png</file>
<file>../../../res/gb-icon.svg</file>
<file>../../../res/gbc-icon-256.png</file>
<file>../../../res/gbc-icon-128.png</file>
<file>../../../res/gbc-icon-32.png</file>
<file>../../../res/gbc-icon-24.png</file>
<file>../../../res/gbc-icon-16.png</file>
<file>../../../res/gbc-icon.svg</file>
<file>../../../res/sgb-icon-256.png</file>
<file>../../../res/sgb-icon-128.png</file>
<file>../../../res/sgb-icon-32.png</file>
<file>../../../res/sgb-icon-24.png</file>
<file>../../../res/sgb-icon-16.png</file>
<file>../../../res/sgb-icon.svg</file>
<file>../../../res/gba-icon-256.png</file>
<file>../../../res/gba-icon-128.png</file>
<file>../../../res/gba-icon-32.png</file>
<file>../../../res/gba-icon-24.png</file>
<file>../../../res/gba-icon-16.png</file>
<file>../../../res/gba-icon.svg</file>
</qresource>
<qresource prefix="/exe">
<file alias="exe4/chip-names.txt">../../../res/exe4/chip-names.txt</file>

View File

@ -0,0 +1,132 @@
/* Copyright (c) 2013-2025 Jeffrey Pfau
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "scripting/AutorunScriptModel.h"
#include "ConfigController.h"
#include "LogController.h"
using namespace QGBA;
AutorunScriptModel::AutorunScriptModel(ConfigController* config, QObject* parent)
: QAbstractListModel(parent)
, m_config(config)
{
QList<QVariant> autorun = m_config->getList("autorunSettings");
for (const auto& item: autorun) {
if (!item.canConvert<ScriptInfo>()) {
continue;
}
m_scripts.append(qvariant_cast<ScriptInfo>(item));
}
}
int AutorunScriptModel::rowCount(const QModelIndex& parent) const {
if (parent.isValid()) {
return 0;
}
return m_scripts.count();
}
bool AutorunScriptModel::setData(const QModelIndex& index, const QVariant& data, int role) {
if (!index.isValid() || index.parent().isValid() || index.row() >= m_scripts.count()) {
return {};
}
switch (role) {
case Qt::CheckStateRole:
m_scripts[index.row()].active = data.value<Qt::CheckState>() == Qt::Checked;
save();
return true;
}
return false;
}
QVariant AutorunScriptModel::data(const QModelIndex& index, int role) const {
if (!index.isValid() || index.parent().isValid() || index.row() >= m_scripts.count()) {
return {};
}
switch (role) {
case Qt::DisplayRole:
return m_scripts.at(index.row()).filename;
case Qt::CheckStateRole:
return m_scripts.at(index.row()).active ? Qt::Checked : Qt::Unchecked;
}
return {};
}
Qt::ItemFlags AutorunScriptModel::flags(const QModelIndex& index) const {
if (!index.isValid() || index.parent().isValid()) {
return Qt::NoItemFlags;
}
return Qt::ItemIsSelectable | Qt::ItemIsUserCheckable | Qt::ItemIsEnabled | Qt::ItemNeverHasChildren;
}
bool AutorunScriptModel::removeRows(int row, int count, const QModelIndex& parent) {
if (parent.isValid()) {
return false;
}
if (m_scripts.size() < row) {
return false;
}
if (m_scripts.size() < row + count) {
count = m_scripts.size() - row;
}
m_scripts.erase(m_scripts.begin() + row, m_scripts.begin() + row + count);
save();
return true;
}
bool AutorunScriptModel::moveRows(const QModelIndex& sourceParent, int sourceRow, int count, const QModelIndex& destinationParent, int destinationChild) {
if (sourceParent.isValid() || destinationParent.isValid()) {
return false;
}
if (sourceRow < 0 || destinationChild < 0) {
return false;
}
if (sourceRow >= m_scripts.size() || destinationChild >= m_scripts.size()) {
return false;
}
if (count > 1) {
LOG(QT, WARN) << tr("Moving more than one row at once is not yet supported");
return false;
}
auto item = m_scripts.takeAt(sourceRow);
m_scripts.insert(destinationChild, item);
save();
return true;
}
void AutorunScriptModel::addScript(const QString& filename) {
beginInsertRows({}, m_scripts.count(), m_scripts.count());
m_scripts.append(ScriptInfo { filename, true });
endInsertRows();
save();
}
QList<QString> AutorunScriptModel::activeScripts() const {
QList<QString> scripts;
for (const auto& pair: m_scripts) {
if (!pair.active) {
continue;
}
scripts.append(pair.filename);
}
return scripts;
}
void AutorunScriptModel::save() {
QList<QVariant> list;
for (const auto& script : m_scripts) {
list.append(QVariant::fromValue(script));
}
m_config->setList("autorunSettings", list);
}

View File

@ -0,0 +1,58 @@
/* Copyright (c) 2013-2025 Jeffrey Pfau
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#pragma once
#include <QAbstractListModel>
#include <QDataStream>
namespace QGBA {
class ConfigController;
class AutorunScriptModel : public QAbstractListModel {
Q_OBJECT
public:
struct ScriptInfo {
QString filename;
bool active;
friend QDataStream& operator<<(QDataStream& stream, const ScriptInfo& object) {
stream << object.filename;
stream << object.active;
return stream;
}
friend QDataStream& operator>>(QDataStream& stream, ScriptInfo& object) {
stream >> object.filename;
stream >> object.active;
return stream;
}
};
AutorunScriptModel(ConfigController* config, QObject* parent = nullptr);
virtual int rowCount(const QModelIndex& parent = QModelIndex()) const override;
virtual bool setData(const QModelIndex& index, const QVariant& data, int role = Qt::DisplayRole) override;
virtual QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
virtual Qt::ItemFlags flags(const QModelIndex& index) const override;
virtual bool removeRows(int row, int count, const QModelIndex& parent = QModelIndex()) override;
virtual bool moveRows(const QModelIndex& sourceParent, int sourceRow, int count, const QModelIndex& destinationParent, int destinationChild) override;
void addScript(const QString& filename);
QList<QString> activeScripts() const;
private:
ConfigController* m_config;
QList<ScriptInfo> m_scripts;
void save();
};
}
Q_DECLARE_METATYPE(QGBA::AutorunScriptModel::ScriptInfo);

View File

@ -0,0 +1,52 @@
/* Copyright (c) 2013-2025 Jeffrey Pfau
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "scripting/AutorunScriptView.h"
#include "GBAApp.h"
#include "scripting/AutorunScriptModel.h"
#include "scripting/ScriptingController.h"
using namespace QGBA;
AutorunScriptView::AutorunScriptView(AutorunScriptModel* model, ScriptingController* controller, QWidget* parent)
: QDialog(parent, Qt::WindowTitleHint | Qt::WindowSystemMenuHint | Qt::WindowCloseButtonHint)
, m_controller(controller)
{
m_ui.setupUi(this);
m_ui.autorunList->setModel(model);
}
void AutorunScriptView::addScript() {
QString filename = GBAApp::app()->getOpenFileName(this, tr("Select a script"), m_controller->getFilenameFilters());
if (filename.isEmpty()) {
return;
}
AutorunScriptModel* model = static_cast<AutorunScriptModel*>(m_ui.autorunList->model());
model->addScript(filename);
}
void AutorunScriptView::removeScript(const QModelIndex& index) {
QAbstractItemModel* model = m_ui.autorunList->model();
model->removeRow(index.row(), index.parent());
}
void AutorunScriptView::removeScript() {
removeScript(m_ui.autorunList->currentIndex());
}
void AutorunScriptView::moveUp() {
QModelIndex index = m_ui.autorunList->currentIndex();
QAbstractItemModel* model = m_ui.autorunList->model();
model->moveRows(index.parent(), index.row(), 1, index.parent(), index.row() - 1);
}
void AutorunScriptView::moveDown() {
QModelIndex index = m_ui.autorunList->currentIndex();
QAbstractItemModel* model = m_ui.autorunList->model();
model->moveRows(index.parent(), index.row(), 1, index.parent(), index.row() + 1);
}

View File

@ -0,0 +1,33 @@
/* Copyright (c) 2013-2025 Jeffrey Pfau
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#pragma once
#include "ui_AutorunScriptView.h"
namespace QGBA {
class AutorunScriptModel;
class ScriptingController;
class AutorunScriptView : public QDialog {
Q_OBJECT
public:
AutorunScriptView(AutorunScriptModel* model, ScriptingController* controller, QWidget* parent = nullptr);
void removeScript(const QModelIndex&);
private slots:
void addScript();
void removeScript();
void moveUp();
void moveDown();
private:
Ui::AutorunScriptView m_ui;
ScriptingController* m_controller;
};
}

View File

@ -0,0 +1,157 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>QGBA::AutorunScriptView</class>
<widget class="QDialog" name="QGBA::AutorunScriptView">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>300</height>
</rect>
</property>
<property name="windowTitle">
<string>Autorun scripts</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="1" column="0">
<widget class="QPushButton" name="add">
<property name="text">
<string>Add</string>
</property>
<property name="icon">
<iconset theme="list-add"/>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QPushButton" name="remove">
<property name="text">
<string>Remove</string>
</property>
<property name="icon">
<iconset theme="list-remove"/>
</property>
</widget>
</item>
<item row="1" column="2">
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="1" column="3">
<widget class="QPushButton" name="up">
<property name="text">
<string>Move up</string>
</property>
<property name="icon">
<iconset theme="go-up"/>
</property>
</widget>
</item>
<item row="1" column="4">
<widget class="QPushButton" name="down">
<property name="text">
<string>Move down</string>
</property>
<property name="icon">
<iconset theme="go-down"/>
</property>
</widget>
</item>
<item row="0" column="0" colspan="5">
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Run scripts when starting a game</string>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="0">
<widget class="QListView" name="autorunList"/>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>add</sender>
<signal>clicked()</signal>
<receiver>QGBA::AutorunScriptView</receiver>
<slot>addScript()</slot>
<hints>
<hint type="sourcelabel">
<x>47</x>
<y>276</y>
</hint>
<hint type="destinationlabel">
<x>199</x>
<y>149</y>
</hint>
</hints>
</connection>
<connection>
<sender>remove</sender>
<signal>clicked()</signal>
<receiver>QGBA::AutorunScriptView</receiver>
<slot>removeScript()</slot>
<hints>
<hint type="sourcelabel">
<x>138</x>
<y>276</y>
</hint>
<hint type="destinationlabel">
<x>199</x>
<y>149</y>
</hint>
</hints>
</connection>
<connection>
<sender>up</sender>
<signal>clicked()</signal>
<receiver>QGBA::AutorunScriptView</receiver>
<slot>moveUp()</slot>
<hints>
<hint type="sourcelabel">
<x>238</x>
<y>276</y>
</hint>
<hint type="destinationlabel">
<x>199</x>
<y>149</y>
</hint>
</hints>
</connection>
<connection>
<sender>down</sender>
<signal>clicked()</signal>
<receiver>QGBA::AutorunScriptView</receiver>
<slot>moveDown()</slot>
<hints>
<hint type="sourcelabel">
<x>341</x>
<y>276</y>
</hint>
<hint type="destinationlabel">
<x>199</x>
<y>149</y>
</hint>
</hints>
</connection>
</connections>
<slots>
<slot>addScript()</slot>
<slot>removeScript()</slot>
<slot>moveUp()</slot>
<slot>moveDown()</slot>
</slots>
</ui>

View File

@ -10,14 +10,17 @@
#include <QMouseEvent>
#include <QWidget>
#include "ConfigController.h"
#include "CoreController.h"
#include "Display.h"
#include "input/Gamepad.h"
#include "input/GamepadButtonEvent.h"
#include "input/GamepadHatEvent.h"
#include "InputController.h"
#include "scripting/AutorunScriptView.h"
#include "scripting/ScriptingTextBuffer.h"
#include "scripting/ScriptingTextBufferModel.h"
#include "Window.h"
#include <mgba/script.h>
#include <mgba-util/math.h>
@ -25,8 +28,9 @@
using namespace QGBA;
ScriptingController::ScriptingController(QObject* parent)
ScriptingController::ScriptingController(ConfigController* config, QObject* parent)
: QObject(parent)
, m_model(config)
{
m_logger.p = this;
m_logger.log = [](mLogger* log, int, enum mLogLevel level, const char* format, va_list args) {
@ -154,9 +158,6 @@ void ScriptingController::reset() {
m_engines.clear();
m_activeEngine = nullptr;
init();
if (m_controller && m_controller->hasStarted()) {
attach();
}
}
void ScriptingController::runCode(const QString& code) {
@ -164,6 +165,11 @@ void ScriptingController::runCode(const QString& code) {
load(vf, "*prompt");
}
void ScriptingController::openAutorunEdit() {
AutorunScriptView* view = new AutorunScriptView(&m_model, this);
emit autorunScriptsOpened(view);
}
void ScriptingController::flushStorage() {
#ifdef USE_JSON_C
mScriptStorageFlushAll(&m_scriptContext);
@ -259,6 +265,15 @@ void ScriptingController::scriptingEvent(QObject* obj, QEvent* event) {
}
}
QString ScriptingController::getFilenameFilters() const {
QStringList filters;
#ifdef USE_LUA
filters.append(tr("Lua scripts (*.lua)"));
#endif
filters.append(tr("All files (*.*)"));
return filters.join(";;");
}
void ScriptingController::updateGamepad() {
InputDriver* driver = m_inputController->gamepadDriver();
if (!driver) {
@ -354,6 +369,14 @@ void ScriptingController::init() {
#ifdef USE_JSON_C
m_storageFlush.start();
#endif
if (m_controller && m_controller->hasStarted()) {
attach();
}
for (const auto& script: m_model.activeScripts()) {
loadFile(script);
}
}
uint32_t ScriptingController::qtToScriptingKey(const QKeyEvent* event) {

View File

@ -13,6 +13,7 @@
#include <mgba/script/input.h>
#include <mgba/core/scripting.h>
#include "scripting/AutorunScriptModel.h"
#include "VFileDevice.h"
#include <memory>
@ -24,6 +25,7 @@ struct VideoBackend;
namespace QGBA {
class ConfigController;
class CoreController;
class InputController;
class ScriptingTextBuffer;
@ -33,7 +35,7 @@ class ScriptingController : public QObject {
Q_OBJECT
public:
ScriptingController(QObject* parent = nullptr);
ScriptingController(ConfigController* config, QObject* parent = nullptr);
~ScriptingController();
void setController(std::shared_ptr<CoreController> controller);
@ -48,17 +50,22 @@ public:
mScriptContext* context() { return &m_scriptContext; }
ScriptingTextBufferModel* textBufferModel() const { return m_bufferModel; }
QString getFilenameFilters() const;
signals:
void log(const QString&);
void warn(const QString&);
void error(const QString&);
void textBufferCreated(ScriptingTextBuffer*);
void autorunScriptsOpened(QWidget* view);
public slots:
void clearController();
void updateVideoScale();
void reset();
void runCode(const QString& code);
void openAutorunEdit();
void flushStorage();
@ -91,6 +98,7 @@ private:
mScriptGamepad m_gamepad;
AutorunScriptModel m_model;
std::shared_ptr<CoreController> m_controller;
InputController* m_inputController = nullptr;

View File

@ -16,6 +16,7 @@ using namespace QGBA;
ScriptingTextBuffer::ScriptingTextBuffer(QObject* parent)
: QObject(parent)
, m_document(this)
{
m_shim.init = &ScriptingTextBuffer::init;
m_shim.deinit = &ScriptingTextBuffer::deinit;

View File

@ -40,6 +40,7 @@ ScriptingView::ScriptingView(ScriptingController* controller, ConfigController*
connect(m_ui.buffers->selectionModel(), &QItemSelectionModel::currentChanged, this, &ScriptingView::selectBuffer);
connect(m_ui.load, &QAction::triggered, this, &ScriptingView::load);
connect(m_ui.loadMostRecent, &QAction::triggered, this, &ScriptingView::loadMostRecent);
connect(m_ui.editAutorunScripts, &QAction::triggered, controller, &ScriptingController::openAutorunEdit);
connect(m_ui.reset, &QAction::triggered, controller, &ScriptingController::reset);
m_mruFiles = m_config->getMRU(ConfigController::MRU::Script);
@ -58,7 +59,7 @@ void ScriptingView::submitRepl() {
}
void ScriptingView::load() {
QString filename = GBAApp::app()->getOpenFileName(this, tr("Select script to load"), getFilters());
QString filename = GBAApp::app()->getOpenFileName(this, tr("Select script to load"), m_controller->getFilenameFilters());
if (!filename.isEmpty()) {
if (!m_controller->loadFile(filename)) {
return;
@ -84,15 +85,6 @@ void ScriptingView::selectBuffer(const QModelIndex& current, const QModelIndex&)
}
}
QString ScriptingView::getFilters() const {
QStringList filters;
#ifdef USE_LUA
filters.append(tr("Lua scripts (*.lua)"));
#endif
filters.append(tr("All files (*.*)"));
return filters.join(";;");
}
void ScriptingView::appendMRU(const QString& fname) {
int index = m_mruFiles.indexOf(fname);
if (index >= 0) {
@ -121,4 +113,4 @@ void ScriptingView::updateMRU() {
void ScriptingView::checkEmptyMRU() {
m_ui.loadMostRecent->setEnabled(!m_mruFiles.isEmpty());
}
}

View File

@ -28,8 +28,6 @@ private slots:
void selectBuffer(const QModelIndex& current, const QModelIndex& = QModelIndex());
private:
QString getFilters() const;
void appendMRU(const QString&);
void updateMRU();
void checkEmptyMRU();

View File

@ -98,9 +98,10 @@
</widget>
<addaction name="load"/>
<addaction name="mru"/>
<addaction name="loadMostRecent"/>
<addaction name="loadMostRecent"/>
<addaction name="separator"/>
<addaction name="reset"/>
<addaction name="editAutorunScripts"/>
</widget>
<addaction name="menuFile"/>
</widget>
@ -110,7 +111,7 @@
<string>Load script...</string>
</property>
</action>
<action name="loadMostRecent">
<action name="loadMostRecent">
<property name="text">
<string>&amp;Load most recent</string>
</property>
@ -125,6 +126,11 @@
<string>0</string>
</property>
</action>
<action name="editAutorunScripts">
<property name="text">
<string>Edit autorun scripts...</string>
</property>
</action>
</widget>
<customwidgets>
<customwidget>

View File

@ -0,0 +1,210 @@
/* Copyright (c) 2013-2022 Jeffrey Pfau
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "platform/qt/library/LibraryModel.h"
#if QT_VERSION >= QT_VERSION_CHECK(5, 11, 0)
#include <QAbstractItemModelTester>
#endif
#include <QSignalSpy>
#include <QTest>
#define FIND_GBA_ROW(gba, gb) \
int gba = findGBARow(); \
if (gba < 0) QFAIL("Could not find gba row"); \
int gb = 1 - gba;
using namespace QGBA;
class LibraryModelTest : public QObject {
Q_OBJECT
private:
LibraryModel* model = nullptr;
int findGBARow() {
for (int i = 0; i < model->rowCount(); i++) {
if (model->index(i, 0).data() == "gba") {
return i;
}
}
return -1;
}
LibraryEntry makeGBA(const QString& name, uint32_t crc) {
LibraryEntry entry;
entry.base = "/gba";
entry.filename = name + ".gba";
entry.fullpath = entry.base + "/" + entry.filename;
entry.title = name;
entry.internalTitle = name.toUpper().toUtf8();
entry.internalCode = entry.internalTitle.replace(" ", "").left(4);
entry.platform = mPLATFORM_GBA;
entry.filesize = entry.fullpath.size() * 4;
entry.crc32 = crc;
return entry;
}
LibraryEntry makeGB(const QString& name, uint32_t crc) {
LibraryEntry entry = makeGBA(name, crc);
entry.base = "/gb";
entry.filename = entry.filename.replace("gba", "gb");
entry.fullpath = entry.fullpath.replace("gba", "gb");
entry.platform = mPLATFORM_GB;
entry.filesize /= 4;
return entry;
}
void addTestGames1() {
model->addEntries({
makeGBA("Test Game", 0x12345678),
makeGBA("Another", 0x23456789),
makeGB("Old Game", 0x87654321),
});
}
void addTestGames2() {
model->addEntries({
makeGBA("Game 3", 0x12345679),
makeGBA("Game 4", 0x2345678A),
makeGBA("Game 5", 0x2345678B),
makeGB("Game 6", 0x87654322),
makeGB("Game 7", 0x87654323),
});
}
void updateGame() {
LibraryEntry game = makeGBA("Another", 0x88888888);
model->updateEntries({ game });
QModelIndex idx = find("Another");
QVERIFY2(idx.isValid(), "game not found");
QCOMPARE(idx.siblingAtColumn(LibraryModel::COL_CRC32).data(Qt::EditRole).toInt(), 0x88888888);
}
void removeGames1() {
model->removeEntries({ "/gba/Another.gba", "/gb/Game 6.gb" });
QVERIFY2(!find("Another").isValid(), "game not removed");
QVERIFY2(!find("Game 6").isValid(), "game not removed");
}
void removeGames2() {
model->removeEntries({ "/gb/Old Game.gb", "/gb/Game 7.gb" });
QVERIFY2(!find("Old Game").isValid(), "game not removed");
QVERIFY2(!find("Game 7").isValid(), "game not removed");
}
QModelIndex find(const QString& name) {
for (int i = 0; i < model->rowCount(); i++) {
QModelIndex idx = model->index(i, 0);
if (idx.data().toString() == name) {
return idx;
}
for (int j = 0; j < model->rowCount(idx); j++) {
QModelIndex child = model->index(j, 0, idx);
if (child.data().toString() == name) {
return child;
}
}
}
return QModelIndex();
}
private slots:
void init() {
model = new LibraryModel();
#if QT_VERSION >= QT_VERSION_CHECK(5, 11, 0)
new QAbstractItemModelTester(model, QAbstractItemModelTester::FailureReportingMode::QtTest, model);
#endif
}
void cleanup() {
delete model;
model = nullptr;
}
void testList() {
addTestGames1();
QCOMPARE(model->rowCount(), 3);
QCOMPARE(model->m_games.size(), 3);
addTestGames2();
QCOMPARE(model->rowCount(), 8);
QCOMPARE(model->m_games.size(), 8);
updateGame();
QCOMPARE(model->m_games.size(), 8);
model->removeEntries({ "/gba/Another.gba", "/gb/Game 6.gb" });
QCOMPARE(model->rowCount(), 6);
QCOMPARE(model->m_games.size(), 6);
model->removeEntries({ "/gb/Old Game.gb", "/gb/Game 7.gb" });
QCOMPARE(model->rowCount(), 4);
QCOMPARE(model->m_games.size(), 4);
}
void testTree() {
model->setTreeMode(true);
addTestGames1();
FIND_GBA_ROW(gbaRow, gbRow);
QCOMPARE(model->rowCount(), 2);
QCOMPARE(model->rowCount(model->index(gbRow, 0)), 1);
QCOMPARE(model->rowCount(model->index(gbaRow, 0)), 2);
QCOMPARE(model->m_games.size(), 3);
addTestGames2();
QCOMPARE(model->rowCount(), 2);
QCOMPARE(model->rowCount(model->index(gbRow, 0)), 3);
QCOMPARE(model->rowCount(model->index(gbaRow, 0)), 5);
QCOMPARE(model->m_games.size(), 8);
updateGame();
QCOMPARE(model->m_games.size(), 8);
removeGames1();
QCOMPARE(model->rowCount(), 2);
QCOMPARE(model->rowCount(model->index(gbRow, 0)), 2);
QCOMPARE(model->rowCount(model->index(gbaRow, 0)), 4);
QCOMPARE(model->m_games.size(), 6);
removeGames2();
QVERIFY2(!find("gb").isValid(), "did not remove gb folder");
QCOMPARE(model->rowCount(), 1);
QCOMPARE(model->rowCount(model->index(0, 0)), 4);
QCOMPARE(model->m_games.size(), 4);
}
void modeSwitchTest1() {
addTestGames1();
{
QSignalSpy resetSpy(model, SIGNAL(modelReset()));
model->setTreeMode(true);
QVERIFY(resetSpy.count());
}
FIND_GBA_ROW(gbaRow, gbRow);
QCOMPARE(model->rowCount(), 2);
QCOMPARE(model->rowCount(model->index(gbRow, 0)), 1);
QCOMPARE(model->rowCount(model->index(gbaRow, 0)), 2);
{
QSignalSpy resetSpy(model, SIGNAL(modelReset()));
model->setTreeMode(false);
QVERIFY(resetSpy.count());
}
addTestGames2();
QCOMPARE(model->rowCount(), 8);
}
void modeSwitchTest2() {
model->setTreeMode(false);
addTestGames1();
model->setTreeMode(true);
FIND_GBA_ROW(gbaRow, gbRow);
QCOMPARE(model->rowCount(), 2);
QCOMPARE(model->rowCount(model->index(gbRow, 0)), 1);
QCOMPARE(model->rowCount(model->index(gbaRow, 0)), 2);
addTestGames2();
QCOMPARE(model->rowCount(), 2);
QCOMPARE(model->rowCount(model->index(gbRow, 0)), 3);
QCOMPARE(model->rowCount(model->index(gbaRow, 0)), 5);
model->setTreeMode(false);
QCOMPARE(model->rowCount(), 8);
}
};
QTEST_MAIN(LibraryModelTest)
#include "library.moc"

View File

@ -0,0 +1,61 @@
/* Copyright (c) 2013-2022 Jeffrey Pfau
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "platform/qt/utils.h"
#include <QTest>
using namespace QGBA;
class SpanSetTest : public QObject {
Q_OBJECT
private:
void debugSpans(const SpanSet& spanSet) {
QStringList debug;
for (auto span : spanSet.spans) {
debug << QStringLiteral("[%1, %2]").arg(span.left).arg(span.right);
}
qDebug() << QStringLiteral("SpanSet{%1}").arg(debug.join(", "));
}
private slots:
void oneSpan() {
SpanSet spanSet;
spanSet.add(1);
spanSet.add(2);
spanSet.add(3);
QCOMPARE(spanSet.spans.size(), 1);
spanSet.merge();
QCOMPARE(spanSet.spans.size(), 1);
}
void twoSpans() {
SpanSet spanSet;
spanSet.add(1);
spanSet.add(2);
spanSet.add(4);
QCOMPARE(spanSet.spans.size(), 2);
spanSet.merge();
QCOMPARE(spanSet.spans.size(), 2);
}
void mergeSpans() {
SpanSet spanSet;
spanSet.add(1);
spanSet.add(3);
spanSet.add(2);
spanSet.add(5);
spanSet.add(4);
spanSet.add(7);
spanSet.add(8);
QCOMPARE(spanSet.spans.size(), 4);
spanSet.merge();
QCOMPARE(spanSet.spans.size(), 2);
}
};
QTEST_APPLESS_MAIN(SpanSetTest)
#include "spanset.moc"

View File

@ -5,7 +5,13 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "utils.h"
#include <mgba/core/library.h>
#ifdef M_CORE_GB
#include <mgba/gb/interface.h>
#endif
#include <QCoreApplication>
#include <QHostAddress>
#include <QKeySequence>
#include <QObject>
@ -29,18 +35,25 @@ QString niceSizeFormat(size_t filesize) {
return unit.arg(size, 0, 'f', int(size * 10) % 10 ? 1 : 0);
}
QString nicePlatformFormat(mPlatform platform) {
QString nicePlatformFormat(mPlatform platform, int validModels) {
switch (platform) {
#ifdef M_CORE_GBA
case mPLATFORM_GBA:
return QObject::tr("GBA");
return "GBA";
#endif
#ifdef M_CORE_GB
case mPLATFORM_GB:
return QObject::tr("GB");
if (validModels != M_LIBRARY_MODEL_UNKNOWN) {
if (validModels & GB_MODEL_CGB) {
return "GBC";
} else if (validModels & GB_MODEL_SGB) {
return "SGB";
}
}
return "GB";
#endif
default:
return QObject::tr("?");
return "?";
}
}
@ -177,4 +190,45 @@ QString keyName(int key) {
}
}
void SpanSet::add(int pos) {
for (Span& span : spans) {
if (pos == span.left - 1) {
span.left = pos;
return;
} else if (pos == span.right + 1) {
span.right = pos;
return;
}
}
spans << Span{ pos, pos };
}
void SpanSet::merge() {
int numSpans = spans.size();
if (!numSpans) {
return;
}
sort();
QVector<Span> merged({ spans[0] });
int lastRight = merged[0].right;
for (int i = 1; i < numSpans; i++) {
int right = spans[i].right;
if (spans[i].left - 1 <= lastRight) {
merged.back().right = right;
} else {
merged << spans[i];
}
lastRight = right;
}
spans = merged;
}
void SpanSet::sort(bool reverse) {
if (reverse) {
std::sort(spans.begin(), spans.end(), std::greater<Span>());
} else {
std::sort(spans.begin(), spans.end());
}
}
}

View File

@ -13,6 +13,7 @@
#include <QRect>
#include <QSize>
#include <QString>
#include <QVector>
#include <algorithm>
#include <functional>
@ -30,7 +31,7 @@ enum class Endian {
};
QString niceSizeFormat(size_t filesize);
QString nicePlatformFormat(mPlatform platform);
QString nicePlatformFormat(mPlatform platform, int validModels = 0);
bool convertAddress(const QHostAddress* input, Address* output);
@ -117,4 +118,29 @@ bool extractMatchingFile(VDir* dir, std::function<QString (VDirEntry*)> filter);
QString keyName(int key);
struct SpanSet {
struct Span {
int left;
int right;
inline bool operator<(const Span& other) const { return left < other.left; }
inline bool operator>(const Span& other) const { return left > other.left; }
};
void add(int pos);
void merge();
void sort(bool reverse = false);
QVector<Span> spans;
};
template<typename T>
QSet<T> qListToSet(const QList<T>& list) {
#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
return QSet<T>::fromList(list);
#else
return QSet<T>(list.begin(), list.end());
#endif
}
}

View File

@ -109,7 +109,7 @@ static void _mSDLAudioCallback(void* context, Uint8* data, int len) {
fauxClock = mCoreCalculateFramerateRatio(audioContext->core, audioContext->sync->fpsTarget);
}
mCoreSyncLockAudio(audioContext->sync);
audioContext->sync->audioHighWater = audioContext->samples + audioContext->resampler.highWaterMark + audioContext->resampler.lowWaterMark;
audioContext->sync->audioHighWater = audioContext->samples + audioContext->resampler.highWaterMark + audioContext->resampler.lowWaterMark + (audioContext->samples >> 6);
audioContext->sync->audioHighWater *= sampleRate / (fauxClock * audioContext->obtainedSpec.freq);
}
mAudioResamplerSetSource(&audioContext->resampler, buffer, sampleRate / fauxClock, true);

View File

@ -396,6 +396,10 @@ void mSDLUpdateJoysticks(struct mSDLEvents* events, const struct Configuration*
events->players[i]->joystick = NULL;
}
struct SDL_JoystickCombo* joystick = _mSDLOpenJoystick(events, event.jdevice.which);
if (!joystick) {
mLOG(SDL_EVENTS, ERROR, "SDL joystick hotplug attach failed: %s", SDL_GetError());
continue;
}
for (i = 0; i < events->playersAttached && i < MAX_PLAYERS; ++i) {
if (joysticks[i] != -1) {

View File

@ -131,6 +131,7 @@ void mScriptContextAttachStdlib(struct mScriptContext* context) {
mScriptContextExportConstants(context, "CHECKSUM", (struct mScriptKVPair[]) {
mSCRIPT_CONSTANT_PAIR(mCHECKSUM, CRC32),
mSCRIPT_CONSTANT_PAIR(mCHECKSUM, MD5),
mSCRIPT_CONSTANT_PAIR(mCHECKSUM, SHA1),
mSCRIPT_KV_SENTINEL
});
#ifdef M_CORE_GBA

View File

@ -1,4 +1,4 @@
cmake_minimum_required (VERSION 3.1.0)
cmake_minimum_required (VERSION 3.10)
project (DiscordRPC)
include(GNUInstallDirs)
@ -9,6 +9,10 @@ file(GLOB_RECURSE ALL_SOURCE_FILES
src/*.cpp src/*.h src/*.c
)
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-class-memaccess")
endif()
# add subdirs
add_subdirectory(src)

View File

@ -7,6 +7,7 @@ set(BASE_SOURCE_FILES
gbk-table.c
hash.c
md5.c
sha1.c
string.c
table.c
vector.c

View File

@ -904,10 +904,10 @@ uint32_t mColorConvert(uint32_t color, enum mColorFormat from, enum mColorFormat
return color;
}
int r = 0;
int g = 0;
int b = 0;
int a = 0xFF;
uint32_t r = 0;
uint32_t g = 0;
uint32_t b = 0;
uint32_t a = 0xFF;
switch (from) {
case mCOLOR_ARGB8:

258
src/util/sha1.c Normal file
View File

@ -0,0 +1,258 @@
/* Copyright (c) 2013-2025 Jeffrey Pfau
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*
* Based on https://github.com/clibs/sha1
*
* Test Vectors (from FIPS PUB 180-1)
* "abc"
* A9993E36 4706816A BA3E2571 7850C26C 9CD0D89D
* "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq"
* 84983E44 1C3BD26E BAAE4AA1 F95129E5 E54670F1
* A million repetitions of "a"
* 34AA973C D4C4DAA4 F61EEB2B DBAD2731 6534016F
*/
#include <mgba-util/sha1.h>
#include <mgba-util/vfs.h>
/* #define SHA1HANDSOFF * Copies data before messing with it. */
#define SHA1HANDSOFF
#define rol(value, bits) (((value) << (bits)) | ((value) >> (32 - (bits))))
/* blk0() and blk() perform the initial expand. */
/* I got the idea of expanding during the round function from SSLeay */
#ifndef __BIG_ENDIAN__
#define blk0(i) (block->l[i] = (rol(block->l[i],24)&0xFF00FF00) \
|(rol(block->l[i],8)&0x00FF00FF))
#else
#define blk0(i) block->l[i]
#endif
#define blk(i) (block->l[i&15] = rol(block->l[(i+13)&15]^block->l[(i+8)&15] \
^block->l[(i+2)&15]^block->l[i&15],1))
/* (R0+R1), R2, R3, R4 are the different operations used in SHA1 */
#define R0(v,w,x,y,z,i) z+=((w&(x^y))^y)+blk0(i)+0x5A827999+rol(v,5);w=rol(w,30);
#define R1(v,w,x,y,z,i) z+=((w&(x^y))^y)+blk(i)+0x5A827999+rol(v,5);w=rol(w,30);
#define R2(v,w,x,y,z,i) z+=(w^x^y)+blk(i)+0x6ED9EBA1+rol(v,5);w=rol(w,30);
#define R3(v,w,x,y,z,i) z+=(((w|x)&y)|(w&x))+blk(i)+0x8F1BBCDC+rol(v,5);w=rol(w,30);
#define R4(v,w,x,y,z,i) z+=(w^x^y)+blk(i)+0xCA62C1D6+rol(v,5);w=rol(w,30);
/* Hash a single 512-bit block. This is the core of the algorithm. */
static void sha1Transform(uint32_t state[5], const uint8_t buffer[64]) {
uint32_t a, b, c, d, e;
typedef union {
unsigned char c[64];
uint32_t l[16];
} CHAR64LONG16;
#ifdef SHA1HANDSOFF
CHAR64LONG16 block[1]; /* use array to appear as a pointer */
memcpy(block, buffer, 64);
#else
/* The following had better never be used because it causes the
* pointer-to-const buffer to be cast into a pointer to non-const.
* And the result is written through. I threw a "const" in, hoping
* this will cause a diagnostic.
*/
CHAR64LONG16 *block = (const CHAR64LONG16 *) buffer;
#endif
/* Copy context->state[] to working vars */
a = state[0];
b = state[1];
c = state[2];
d = state[3];
e = state[4];
/* 4 rounds of 20 operations each. Loop unrolled. */
R0(a, b, c, d, e, 0);
R0(e, a, b, c, d, 1);
R0(d, e, a, b, c, 2);
R0(c, d, e, a, b, 3);
R0(b, c, d, e, a, 4);
R0(a, b, c, d, e, 5);
R0(e, a, b, c, d, 6);
R0(d, e, a, b, c, 7);
R0(c, d, e, a, b, 8);
R0(b, c, d, e, a, 9);
R0(a, b, c, d, e, 10);
R0(e, a, b, c, d, 11);
R0(d, e, a, b, c, 12);
R0(c, d, e, a, b, 13);
R0(b, c, d, e, a, 14);
R0(a, b, c, d, e, 15);
R1(e, a, b, c, d, 16);
R1(d, e, a, b, c, 17);
R1(c, d, e, a, b, 18);
R1(b, c, d, e, a, 19);
R2(a, b, c, d, e, 20);
R2(e, a, b, c, d, 21);
R2(d, e, a, b, c, 22);
R2(c, d, e, a, b, 23);
R2(b, c, d, e, a, 24);
R2(a, b, c, d, e, 25);
R2(e, a, b, c, d, 26);
R2(d, e, a, b, c, 27);
R2(c, d, e, a, b, 28);
R2(b, c, d, e, a, 29);
R2(a, b, c, d, e, 30);
R2(e, a, b, c, d, 31);
R2(d, e, a, b, c, 32);
R2(c, d, e, a, b, 33);
R2(b, c, d, e, a, 34);
R2(a, b, c, d, e, 35);
R2(e, a, b, c, d, 36);
R2(d, e, a, b, c, 37);
R2(c, d, e, a, b, 38);
R2(b, c, d, e, a, 39);
R3(a, b, c, d, e, 40);
R3(e, a, b, c, d, 41);
R3(d, e, a, b, c, 42);
R3(c, d, e, a, b, 43);
R3(b, c, d, e, a, 44);
R3(a, b, c, d, e, 45);
R3(e, a, b, c, d, 46);
R3(d, e, a, b, c, 47);
R3(c, d, e, a, b, 48);
R3(b, c, d, e, a, 49);
R3(a, b, c, d, e, 50);
R3(e, a, b, c, d, 51);
R3(d, e, a, b, c, 52);
R3(c, d, e, a, b, 53);
R3(b, c, d, e, a, 54);
R3(a, b, c, d, e, 55);
R3(e, a, b, c, d, 56);
R3(d, e, a, b, c, 57);
R3(c, d, e, a, b, 58);
R3(b, c, d, e, a, 59);
R4(a, b, c, d, e, 60);
R4(e, a, b, c, d, 61);
R4(d, e, a, b, c, 62);
R4(c, d, e, a, b, 63);
R4(b, c, d, e, a, 64);
R4(a, b, c, d, e, 65);
R4(e, a, b, c, d, 66);
R4(d, e, a, b, c, 67);
R4(c, d, e, a, b, 68);
R4(b, c, d, e, a, 69);
R4(a, b, c, d, e, 70);
R4(e, a, b, c, d, 71);
R4(d, e, a, b, c, 72);
R4(c, d, e, a, b, 73);
R4(b, c, d, e, a, 74);
R4(a, b, c, d, e, 75);
R4(e, a, b, c, d, 76);
R4(d, e, a, b, c, 77);
R4(c, d, e, a, b, 78);
R4(b, c, d, e, a, 79);
/* Add the working vars back into context.state[] */
state[0] += a;
state[1] += b;
state[2] += c;
state[3] += d;
state[4] += e;
/* Wipe variables */
a = b = c = d = e = 0;
#ifdef SHA1HANDSOFF
memset(block, '\0', sizeof(block));
#endif
}
/* shaInit - Initialize new context */
void sha1Init(struct SHA1Context* context) {
/* SHA1 initialization constants */
context->state[0] = 0x67452301;
context->state[1] = 0xEFCDAB89;
context->state[2] = 0x98BADCFE;
context->state[3] = 0x10325476;
context->state[4] = 0xC3D2E1F0;
context->count[0] = context->count[1] = 0;
}
/* Run your data through this. */
void sha1Update(struct SHA1Context* context, const void* data, size_t len) {
size_t i;
size_t j;
j = context->count[0];
if ((context->count[0] += len << 3) < j) {
++context->count[1];
}
context->count[1] += (len >> 29);
j = (j >> 3) & 63;
if ((j + len) > 63) {
memcpy(&context->buffer[j], data, (i = 64 - j));
sha1Transform(context->state, context->buffer);
for (; i + 63 < len; i += 64) {
sha1Transform(context->state, &((uint8_t*) data)[i]);
}
j = 0;
} else {
i = 0;
}
memcpy(&context->buffer[j], &((uint8_t*) data)[i], len - i);
}
/* Add padding and return the message digest. */
void sha1Finalize(uint8_t digest[20], struct SHA1Context* context) {
unsigned i;
uint8_t finalcount[8];
uint8_t c;
for (i = 0; i < 8; ++i) {
finalcount[i] = (uint8_t) ((context->count[(i >= 4 ? 0 : 1)] >> ((3 - (i & 3)) * 8)) & 255); /* Endian independent */
}
c = 0200;
sha1Update(context, &c, 1);
while ((context->count[0] & 504) != 448) {
c = 0000;
sha1Update(context, &c, 1);
}
sha1Update(context, finalcount, 8); /* Should cause a SHA1Transform() */
for (i = 0; i < 20; ++i) {
digest[i] = (uint8_t) ((context->state[i >> 2] >> ((3 - (i & 3)) * 8)) & 255);
}
/* Wipe variables */
memset(context, '\0', sizeof(*context));
memset(&finalcount, '\0', sizeof(finalcount));
}
void sha1Buffer(const void* input, size_t len, uint8_t* result) {
struct SHA1Context ctx;
size_t i;
sha1Init(&ctx);
for (i = 0; i + 63 < len; i += 64) {
sha1Update(&ctx, &((const uint8_t*) input)[i], 64);
}
for (; i < len; ++i) {
sha1Update(&ctx, &((const uint8_t*) input)[i], 1);
}
sha1Finalize(result, &ctx);
}
bool sha1File(struct VFile* vf, uint8_t* result) {
struct SHA1Context ctx;
uint8_t buffer[2048];
sha1Init(&ctx);
ssize_t read;
ssize_t position = vf->seek(vf, 0, SEEK_CUR);
if (vf->seek(vf, 0, SEEK_SET) < 0) {
return false;
}
while ((read = vf->read(vf, buffer, sizeof(buffer))) > 0) {
sha1Update(&ctx, buffer, read);
}
vf->seek(vf, position, SEEK_SET);
if (read < 0) {
return false;
}
sha1Finalize(result, &ctx);
return true;
}

View File

@ -7,6 +7,7 @@
#include <mgba-util/crc32.h>
#include <mgba-util/md5.h>
#include <mgba-util/sha1.h>
M_TEST_DEFINE(emptyCrc32) {
uint8_t buffer[1] = {0};
@ -115,6 +116,92 @@ M_TEST_DEFINE(twoBlockMd5) {
}), 16);
}
M_TEST_DEFINE(emptySha1) {
uint8_t buffer[1] = {0};
uint8_t digest[20] = {0};
sha1Buffer(buffer, 0, digest);
assert_memory_equal(digest, ((uint8_t[]) {
0xDA, 0x39, 0xA3, 0xEE, 0x5E, 0x6B, 0x4B, 0x0D, 0x32, 0x55,
0xBF, 0xEF, 0x95, 0x60, 0x18, 0x90, 0xAF, 0xD8, 0x07, 0x09
}), 16);
}
M_TEST_DEFINE(newlineSha1) {
uint8_t buffer[1] = { '\n' };
uint8_t digest[20] = {0};
sha1Buffer(buffer, 1, digest);
assert_memory_equal(digest, ((uint8_t[]) {
0xAD, 0xC8, 0x3B, 0x19, 0xE7, 0x93, 0x49, 0x1B, 0x1C, 0x6E,
0xA0, 0xFD, 0x8B, 0x46, 0xCD, 0x9F, 0x32, 0xE5, 0x92, 0xFC
}), 20);
}
M_TEST_DEFINE(fullBlockSha1) {
uint8_t buffer[64] = {
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
};
uint8_t digest[20] = {0};
sha1Buffer(buffer, 64, digest);
assert_memory_equal(digest, ((uint8_t[]) {
0xCB, 0x4D, 0xD3, 0xDA, 0xCA, 0x2D, 0x6F, 0x25, 0x44, 0xBC,
0x0D, 0xAA, 0x6B, 0xEB, 0xB7, 0x8A, 0xED, 0x0B, 0xD0, 0x34
}), 20);
}
M_TEST_DEFINE(overflowBlockSha1) {
uint8_t buffer[65] = {
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x0a,
};
uint8_t digest[20] = {0};
sha1Buffer(buffer, 65, digest);
assert_memory_equal(digest, ((uint8_t[]) {
0xA3, 0x96, 0x68, 0x5E, 0xF7, 0x73, 0x87, 0x13, 0x2C, 0x43,
0x64, 0x42, 0x2D, 0x16, 0x65, 0x39, 0x65, 0x6F, 0xB8, 0x93
}), 20);
}
M_TEST_DEFINE(twoBlockSha1) {
uint8_t buffer[128] = {
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
};
uint8_t digest[20] = {0};
sha1Buffer(buffer, 128, digest);
assert_memory_equal(digest, ((uint8_t[]) {
0xFF, 0xB5, 0xE5, 0xD9, 0x6E, 0x19, 0x71, 0x4F, 0xFE, 0xF6,
0x0A, 0xC8, 0x74, 0x9E, 0xCA, 0xEF, 0xBE, 0xC9, 0xD2, 0x95
}), 20);
}
M_TEST_SUITE_DEFINE(Hashes,
cmocka_unit_test(emptyCrc32),
cmocka_unit_test(newlineCrc32),
@ -127,4 +214,9 @@ M_TEST_SUITE_DEFINE(Hashes,
cmocka_unit_test(fullBlockMd5),
cmocka_unit_test(overflowBlockMd5),
cmocka_unit_test(twoBlockMd5),
cmocka_unit_test(emptySha1),
cmocka_unit_test(newlineSha1),
cmocka_unit_test(fullBlockSha1),
cmocka_unit_test(overflowBlockSha1),
cmocka_unit_test(twoBlockSha1),
)

View File

@ -72,14 +72,33 @@ M_TEST_DEFINE(openNullMemChunkNonzero) {
}
M_TEST_DEFINE(resizeMem) {
uint8_t bytes[32];
uint8_t bytes[32] = {
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F,
0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17,
0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F
};
struct VFile* vf = VFileFromMemory(bytes, 32);
assert_non_null(vf);
assert_int_equal(vf->seek(vf, 0, SEEK_END), 32);
assert_int_equal(vf->size(vf), 32);
vf->truncate(vf, 64);
assert_int_equal(vf->size(vf), 32);
assert_int_equal(bytes[15], 0x0F);
assert_int_equal(bytes[16], 0x10);
assert_int_equal(vf->seek(vf, 0, SEEK_CUR), 32);
vf->truncate(vf, 16);
assert_int_equal(vf->size(vf), 16);
assert_int_equal(vf->seek(vf, 0, SEEK_CUR), 16);
vf->truncate(vf, 64);
assert_int_equal(vf->size(vf), 32);
assert_int_equal(bytes[15], 0x0F);
assert_int_equal(bytes[16], 0x00);
assert_int_equal(vf->seek(vf, 0, SEEK_CUR), 16);
vf->close(vf);
}
@ -87,11 +106,16 @@ M_TEST_DEFINE(resizeConstMem) {
uint8_t bytes[32] = {0};
struct VFile* vf = VFileFromConstMemory(bytes, 32);
assert_non_null(vf);
assert_int_equal(vf->seek(vf, 0, SEEK_END), 32);
assert_int_equal(vf->size(vf), 32);
vf->truncate(vf, 64);
assert_int_equal(vf->size(vf), 32);
assert_int_equal(vf->seek(vf, 0, SEEK_CUR), 32);
vf->truncate(vf, 16);
assert_int_equal(vf->size(vf), 32);
assert_int_equal(vf->seek(vf, 0, SEEK_CUR), 32);
vf->close(vf);
}
@ -99,11 +123,16 @@ M_TEST_DEFINE(resizeMemChunk) {
uint8_t bytes[32] = {0};
struct VFile* vf = VFileMemChunk(bytes, 32);
assert_non_null(vf);
assert_int_equal(vf->seek(vf, 0, SEEK_END), 32);
assert_int_equal(vf->size(vf), 32);
vf->truncate(vf, 64);
assert_int_equal(vf->size(vf), 64);
assert_int_equal(vf->seek(vf, 0, SEEK_CUR), 32);
vf->truncate(vf, 16);
assert_int_equal(vf->size(vf), 16);
assert_int_equal(vf->seek(vf, 0, SEEK_CUR), 16);
vf->close(vf);
}

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