mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-10 13:33:24 -05:00
Compare commits
31
Commits
8e3a486107
...
27610c1d33
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
27610c1d33 | ||
|
|
25034cd958 | ||
|
|
480c02882d | ||
|
|
056953d833 | ||
|
|
49d425aa8a | ||
|
|
58cb61a08f | ||
|
|
485e5bc2b3 | ||
|
|
c5165ffb96 | ||
|
|
183793481c | ||
|
|
a762152c12 | ||
|
|
b650f9eb8a | ||
|
|
4ca4592f65 | ||
|
|
0fb09c6c94 | ||
|
|
34e382eae8 | ||
|
|
9e327a05ed | ||
|
|
f010e737c5 | ||
|
|
edce60bae1 | ||
|
|
44663db144 | ||
|
|
757fad69bc | ||
|
|
ca6462c510 | ||
|
|
3f5d825ac6 | ||
|
|
151dd5ea04 | ||
|
|
eaf315ea8f | ||
|
|
366598e662 | ||
|
|
5f3e9fbacd | ||
|
|
045372992c | ||
|
|
38ffccc465 | ||
|
|
68bcef544d | ||
|
|
81995a4c18 | ||
|
|
95d14819ce | ||
|
|
dc31a90dca |
No files matched your search
@@ -72,9 +72,9 @@ Each file is checked the way your platform checks it. Most compare a checksum, t
|
||||
- **12 platforms** supported with platform-specific verification
|
||||
- **506 emulators** profiled from source (RetroArch cores + standalone)
|
||||
- **546 systems** handled by those emulators (NES, SNES, PlayStation, Saturn, Dreamcast, ...)
|
||||
- **18,983 files in the collection**, each with its SHA1, MD5, SHA256, CRC32 and Adler-32 fingerprints: 5,253 system files, 2,810 arcade ROM sets, 10,920 game and engine data files. That is every platform and emulator together, so no pack holds them all: each carries what its own emulators load, counted in the download table
|
||||
- **561 of 5,253 system files** matched to dump-preservation catalogs (No-Intro, Redump, TOSEC); arcade sets and engine data fall outside what those catalogs index
|
||||
- **16668 MB** total collection size
|
||||
- **18,988 files in the collection**, each with its SHA1, MD5, SHA256, CRC32 and Adler-32 fingerprints: 5,258 system files, 2,810 arcade ROM sets, 10,920 game and engine data files. That is every platform and emulator together, so no pack holds them all: each carries what its own emulators load, counted in the download table
|
||||
- **562 of 5,258 system files** matched to dump-preservation catalogs (No-Intro, Redump, TOSEC); arcade sets and engine data fall outside what those catalogs index
|
||||
- **16670 MB** total collection size
|
||||
|
||||
## Supported systems
|
||||
|
||||
@@ -86,17 +86,17 @@ Full list with per-file details: **[https://abdess.github.io/retrobios/](https:/
|
||||
|
||||
| Platform | On its BIOS list | Files its emulators load | Checked by |
|
||||
|----------|-----------------:|-------------------------:|------------|
|
||||
| Batocera | 353/353 | 12,275/12,705 | MD5 hash |
|
||||
| BizHawk | 118/118 | 647/648 | SHA1 hash |
|
||||
| Batocera | 353/353 | 12,274/12,704 | MD5 hash |
|
||||
| BizHawk | 118/118 | 649/650 | SHA1 hash |
|
||||
| EmuDeck | 168/168 | 381/383 | MD5 hash |
|
||||
| Lakka | 530/530 | 5,262/5,292 | file presence |
|
||||
| Lakka | 527/527 | 5,263/5,293 | file presence |
|
||||
| MiSTer FPGA | 81/81 | - | MD5 hash |
|
||||
| ROCKNIX | 38/38 | 5,700/5,730 | MD5 hash |
|
||||
| Recalbox | 351/351 | 2,726/2,734 | MD5 hash |
|
||||
| RetroArch | 530/530 | 5,262/5,292 | file presence |
|
||||
| Recalbox | 351/351 | 2,727/2,735 | MD5 hash |
|
||||
| RetroArch | 527/527 | 5,263/5,293 | file presence |
|
||||
| RetroBat | 348/348 | 11,748/12,178 | MD5 hash |
|
||||
| RetroDECK | 2,008/2,008 | 5,279/5,329 | MD5 hash |
|
||||
| RetroPie * | 530/530 | 9,181/9,323 | file presence |
|
||||
| RetroDECK | 2,008/2,008 | 5,280/5,330 | MD5 hash |
|
||||
| RetroPie * | 527/527 | 9,182/9,324 | file presence |
|
||||
| RomM | 381/381 | 624/630 | MD5 hash |
|
||||
|
||||
Each fraction is what the pack has over what is needed, counting required and optional files alike since both ship. The first column is the BIOS list the platform publishes. The second counts files its emulators load that this list never mentions, found by reading their source code, and it is routinely several times larger. A short fraction means files are still missing, and they are named in the [gap analysis](https://abdess.github.io/retrobios/gaps/).
|
||||
@@ -180,4 +180,4 @@ The scripts and tooling are released under the [MIT License](LICENSE).
|
||||
The BIOS and firmware files are not covered by that license: they are third-party system software, preserved and provided for personal backup, archival, and interoperability with emulation software. [NOTICE](NOTICE) sets out their status and how to ask for a file to be removed.
|
||||
The reasoning, and where it is weakest, is in the [FAQ](https://abdess.github.io/retrobios/wiki/faq/#is-this-legal).
|
||||
|
||||
*Auto-generated on 2026-10-05T00:47:41Z*
|
||||
*Auto-generated on 2026-10-05T23:10:58Z*
|
||||
Binary file not shown.
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,400 @@
|
||||
#ifndef PS_SCALE_FACTOR
|
||||
#define PS_SCALE_FACTOR 1.0
|
||||
#endif
|
||||
|
||||
#ifdef VERTEX_SHADER
|
||||
|
||||
layout(location = 0) in vec4 a_pos;
|
||||
layout(location = 1) in vec2 a_tex;
|
||||
|
||||
layout(location = 0) out vec2 v_tex;
|
||||
|
||||
void main()
|
||||
{
|
||||
gl_Position = vec4(a_pos.x, -a_pos.y, a_pos.z, a_pos.w);
|
||||
v_tex = a_tex;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
#ifdef FRAGMENT_SHADER
|
||||
|
||||
layout(location = 0) in vec2 v_tex;
|
||||
|
||||
#if defined(ps_convert_rgba8_16bits) || defined(ps_convert_float32_32bits)
|
||||
layout(location = 0) out uint o_col0;
|
||||
#elif !defined(ps_datm1) && \
|
||||
!defined(ps_datm0) && \
|
||||
!defined(ps_convert_rgba8_float32) && \
|
||||
!defined(ps_convert_rgba8_float24) && \
|
||||
!defined(ps_convert_rgba8_float16) && \
|
||||
!defined(ps_convert_rgb5a1_float16) && \
|
||||
!defined(ps_convert_rgba8_float32_biln) && \
|
||||
!defined(ps_convert_rgba8_float24_biln) && \
|
||||
!defined(ps_convert_rgba8_float16_biln) && \
|
||||
!defined(ps_convert_rgb5a1_float16_biln) && \
|
||||
!defined(ps_depth_copy)
|
||||
layout(location = 0) out vec4 o_col0;
|
||||
#endif
|
||||
|
||||
layout(set = 0, binding = 0) uniform sampler2D samp0;
|
||||
|
||||
vec4 sample_c(vec2 uv)
|
||||
{
|
||||
return texture(samp0, uv);
|
||||
}
|
||||
|
||||
#ifdef ps_copy
|
||||
void ps_copy()
|
||||
{
|
||||
o_col0 = sample_c(v_tex);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef ps_depth_copy
|
||||
void ps_depth_copy()
|
||||
{
|
||||
gl_FragDepth = sample_c(v_tex).r;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef ps_filter_transparency
|
||||
void ps_filter_transparency()
|
||||
{
|
||||
vec4 c = sample_c(v_tex);
|
||||
o_col0 = vec4(c.rgb, 1.0);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef ps_convert_rgba8_16bits
|
||||
// Need to be careful with precision here, it can break games like Spider-Man 3 and Dogs Life
|
||||
void ps_convert_rgba8_16bits()
|
||||
{
|
||||
uvec4 i = uvec4(sample_c(v_tex) * vec4(255.5f, 255.5f, 255.5f, 255.5f));
|
||||
|
||||
o_col0 = ((i.x & 0x00F8u) >> 3) | ((i.y & 0x00F8u) << 2) | ((i.z & 0x00f8u) << 7) | ((i.w & 0x80u) << 8);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef ps_datm1
|
||||
void ps_datm1()
|
||||
{
|
||||
if(sample_c(v_tex).a < (127.5f / 255.0f)) // >= 0x80 pass
|
||||
discard;
|
||||
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef ps_datm0
|
||||
void ps_datm0()
|
||||
{
|
||||
if((127.5f / 255.0f) < sample_c(v_tex).a) // < 0x80 pass (== 0x80 should not pass)
|
||||
discard;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef ps_hdr_init
|
||||
void ps_hdr_init()
|
||||
{
|
||||
vec4 value = sample_c(v_tex);
|
||||
o_col0 = vec4(roundEven(value.rgb * 255.0f) / 65535.0f, value.a);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef ps_hdr_resolve
|
||||
void ps_hdr_resolve()
|
||||
{
|
||||
vec4 value = sample_c(v_tex);
|
||||
o_col0 = vec4(vec3(uvec3(value.rgb * 65535.5f) & 255u) / 255.0f, value.a);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef ps_convert_float32_32bits
|
||||
void ps_convert_float32_32bits()
|
||||
{
|
||||
// Convert a vec32 depth texture into a 32 bits UINT texture
|
||||
o_col0 = uint(exp2(32.0f) * sample_c(v_tex).r);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef ps_convert_float32_rgba8
|
||||
void ps_convert_float32_rgba8()
|
||||
{
|
||||
// Convert a vec32 depth texture into a RGBA color texture
|
||||
uint d = uint(sample_c(v_tex).r * exp2(32.0f));
|
||||
o_col0 = vec4(uvec4((d & 0xFFu), ((d >> 8) & 0xFFu), ((d >> 16) & 0xFFu), (d >> 24))) / vec4(255.0);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef ps_convert_float16_rgb5a1
|
||||
void ps_convert_float16_rgb5a1()
|
||||
{
|
||||
// Convert a vec32 (only 16 lsb) depth into a RGB5A1 color texture
|
||||
uint d = uint(sample_c(v_tex).r * exp2(32.0f));
|
||||
o_col0 = vec4(uvec4((d & 0x1Fu), ((d >> 5) & 0x1Fu), ((d >> 10) & 0x1Fu), (d >> 15) & 0x01u)) / vec4(32.0f, 32.0f, 32.0f, 1.0f);
|
||||
}
|
||||
#endif
|
||||
|
||||
float rgba8_to_depth32(vec4 unorm)
|
||||
{
|
||||
uvec4 c = uvec4(unorm * vec4(255.5f));
|
||||
return float(c.r | (c.g << 8) | (c.b << 16) | (c.a << 24)) * exp2(-32.0f);
|
||||
}
|
||||
|
||||
float rgba8_to_depth24(vec4 unorm)
|
||||
{
|
||||
uvec3 c = uvec3(unorm.rgb * vec3(255.5f));
|
||||
return float(c.r | (c.g << 8) | (c.b << 16)) * exp2(-32.0f);
|
||||
}
|
||||
|
||||
float rgba8_to_depth16(vec4 unorm)
|
||||
{
|
||||
uvec2 c = uvec2(unorm.rg * vec2(255.5f));
|
||||
return float(c.r | (c.g << 8)) * exp2(-32.0f);
|
||||
}
|
||||
|
||||
float rgb5a1_to_depth16(vec4 unorm)
|
||||
{
|
||||
uvec4 c = uvec4(unorm * vec4(255.5f));
|
||||
return float(((c.r & 0xF8u) >> 3) | ((c.g & 0xF8u) << 2) | ((c.b & 0xF8u) << 7) | ((c.a & 0x80u) << 8)) * exp2(-32.0f);
|
||||
}
|
||||
|
||||
#ifdef ps_convert_rgba8_float32
|
||||
void ps_convert_rgba8_float32()
|
||||
{
|
||||
// Convert an RGBA texture into a float depth texture
|
||||
gl_FragDepth = rgba8_to_depth32(sample_c(v_tex));
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef ps_convert_rgba8_float24
|
||||
void ps_convert_rgba8_float24()
|
||||
{
|
||||
// Same as above but without the alpha channel (24 bits Z)
|
||||
|
||||
// Convert an RGBA texture into a float depth texture
|
||||
gl_FragDepth = rgba8_to_depth24(sample_c(v_tex));
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef ps_convert_rgba8_float16
|
||||
void ps_convert_rgba8_float16()
|
||||
{
|
||||
// Same as above but without the A/B channels (16 bits Z)
|
||||
|
||||
// Convert an RGBA texture into a float depth texture
|
||||
gl_FragDepth = rgba8_to_depth16(sample_c(v_tex));
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef ps_convert_rgb5a1_float16
|
||||
void ps_convert_rgb5a1_float16()
|
||||
{
|
||||
// Convert an RGB5A1 (saved as RGBA8) color to a 16 bit Z
|
||||
gl_FragDepth = rgb5a1_to_depth16(sample_c(v_tex));
|
||||
}
|
||||
#endif
|
||||
|
||||
#define SAMPLE_RGBA_DEPTH_BILN(CONVERT_FN) \
|
||||
ivec2 dims = textureSize(samp0, 0); \
|
||||
vec2 top_left_f = v_tex * vec2(dims) - 0.5f; \
|
||||
ivec2 top_left = ivec2(floor(top_left_f)); \
|
||||
ivec4 coords = clamp(ivec4(top_left, top_left + 1), ivec4(0), dims.xyxy - 1); \
|
||||
vec2 mix_vals = fract(top_left_f); \
|
||||
float depthTL = CONVERT_FN(texelFetch(samp0, coords.xy, 0)); \
|
||||
float depthTR = CONVERT_FN(texelFetch(samp0, coords.zy, 0)); \
|
||||
float depthBL = CONVERT_FN(texelFetch(samp0, coords.xw, 0)); \
|
||||
float depthBR = CONVERT_FN(texelFetch(samp0, coords.zw, 0)); \
|
||||
gl_FragDepth = mix(mix(depthTL, depthTR, mix_vals.x), mix(depthBL, depthBR, mix_vals.x), mix_vals.y);
|
||||
|
||||
#ifdef ps_convert_rgba8_float32_biln
|
||||
void ps_convert_rgba8_float32_biln()
|
||||
{
|
||||
// Convert an RGBA texture into a float depth texture
|
||||
SAMPLE_RGBA_DEPTH_BILN(rgba8_to_depth32);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef ps_convert_rgba8_float24_biln
|
||||
void ps_convert_rgba8_float24_biln()
|
||||
{
|
||||
// Same as above but without the alpha channel (24 bits Z)
|
||||
|
||||
// Convert an RGBA texture into a float depth texture
|
||||
SAMPLE_RGBA_DEPTH_BILN(rgba8_to_depth24);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef ps_convert_rgba8_float16_biln
|
||||
void ps_convert_rgba8_float16_biln()
|
||||
{
|
||||
// Same as above but without the A/B channels (16 bits Z)
|
||||
|
||||
// Convert an RGBA texture into a float depth texture
|
||||
SAMPLE_RGBA_DEPTH_BILN(rgba8_to_depth16);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef ps_convert_rgb5a1_float16_biln
|
||||
void ps_convert_rgb5a1_float16_biln()
|
||||
{
|
||||
// Convert an RGB5A1 (saved as RGBA8) color to a 16 bit Z
|
||||
SAMPLE_RGBA_DEPTH_BILN(rgb5a1_to_depth16);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef ps_convert_rgba_8i
|
||||
void ps_convert_rgba_8i()
|
||||
{
|
||||
// Convert a RGBA texture into a 8 bits packed texture
|
||||
// Input column: 8x2 RGBA pixels
|
||||
// 0: 8 RGBA
|
||||
// 1: 8 RGBA
|
||||
// Output column: 16x4 Index pixels
|
||||
// 0: 8 R | 8 B
|
||||
// 1: 8 R | 8 B
|
||||
// 2: 8 G | 8 A
|
||||
// 3: 8 G | 8 A
|
||||
uvec2 pos = uvec2(gl_FragCoord.xy);
|
||||
|
||||
// Collapse separate R G B A areas into their base pixel
|
||||
uvec2 block = (pos & ~uvec2(15u, 3u)) >> 1;
|
||||
uvec2 subblock = pos & uvec2(7u, 1u);
|
||||
uvec2 coord = block | subblock;
|
||||
|
||||
// Apply offset to cols 1 and 2
|
||||
uint is_col23 = pos.y & 4u;
|
||||
uint is_col13 = pos.y & 2u;
|
||||
uint is_col12 = is_col23 ^ (is_col13 << 1);
|
||||
coord.x ^= is_col12; // If cols 1 or 2, flip bit 3 of x
|
||||
|
||||
if (floor(PS_SCALE_FACTOR) != PS_SCALE_FACTOR)
|
||||
coord = uvec2(vec2(coord) * PS_SCALE_FACTOR);
|
||||
else
|
||||
coord *= uvec2(PS_SCALE_FACTOR);
|
||||
|
||||
vec4 pixel = texelFetch(samp0, ivec2(coord), 0);
|
||||
vec2 sel0 = (pos.y & 2u) == 0u ? pixel.rb : pixel.ga;
|
||||
float sel1 = (pos.x & 8u) == 0u ? sel0.x : sel0.y;
|
||||
o_col0 = vec4(sel1); // Divide by something here?
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef ps_convert_clut_4
|
||||
layout(push_constant) uniform cb10
|
||||
{
|
||||
vec2 scale;
|
||||
uvec2 offset;
|
||||
uint doffset;
|
||||
};
|
||||
|
||||
void ps_convert_clut_4()
|
||||
{
|
||||
// CLUT4 is easy, just two rows of 8x8.
|
||||
uint index = uint(gl_FragCoord.x) + doffset;
|
||||
uvec2 pos = uvec2(index % 8u, index / 8u);
|
||||
|
||||
ivec2 final = ivec2(floor(vec2(offset + pos) * vec2(scale)));
|
||||
o_col0 = texelFetch(samp0, final, 0);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef ps_convert_clut_8
|
||||
layout(push_constant) uniform cb10
|
||||
{
|
||||
vec2 scale;
|
||||
uvec2 offset;
|
||||
uint doffset;
|
||||
};
|
||||
|
||||
void ps_convert_clut_8()
|
||||
{
|
||||
uint index = min(uint(gl_FragCoord.x) + doffset, 255u);
|
||||
|
||||
// CLUT is arranged into 8 groups of 16x2, with the top-right and bottom-left quadrants swapped.
|
||||
// This can probably be done better..
|
||||
uint subgroup = (index / 8u) % 4u;
|
||||
uvec2 pos;
|
||||
pos.x = (index % 8u) + ((subgroup >= 2u) ? 8u : 0u);
|
||||
pos.y = ((index / 32u) * 2u) + (subgroup % 2u);
|
||||
|
||||
ivec2 final = ivec2(floor(vec2(offset + pos) * vec2(scale)));
|
||||
o_col0 = texelFetch(samp0, final, 0);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef ps_yuv
|
||||
layout(push_constant) uniform cb10
|
||||
{
|
||||
int EMODA;
|
||||
int EMODC;
|
||||
};
|
||||
|
||||
void ps_yuv()
|
||||
{
|
||||
vec4 i = sample_c(v_tex);
|
||||
vec4 o = vec4(0.0f);
|
||||
|
||||
mat3 rgb2yuv;
|
||||
rgb2yuv[0] = vec3(0.587, -0.311, -0.419);
|
||||
rgb2yuv[1] = vec3(0.114, 0.500, -0.081);
|
||||
rgb2yuv[2] = vec3(0.299, -0.169, 0.500);
|
||||
|
||||
vec3 yuv = rgb2yuv * i.gbr;
|
||||
|
||||
float Y = float(0xDB)/255.0f * yuv.x + float(0x10)/255.0f;
|
||||
float Cr = float(0xE0)/255.0f * yuv.y + float(0x80)/255.0f;
|
||||
float Cb = float(0xE0)/255.0f * yuv.z + float(0x80)/255.0f;
|
||||
|
||||
switch(EMODA) {
|
||||
case 0:
|
||||
o.a = i.a;
|
||||
break;
|
||||
case 1:
|
||||
o.a = Y;
|
||||
break;
|
||||
case 2:
|
||||
o.a = Y/2.0f;
|
||||
break;
|
||||
case 3:
|
||||
o.a = 0.0f;
|
||||
break;
|
||||
}
|
||||
|
||||
switch(EMODC) {
|
||||
case 0:
|
||||
o.rgb = i.rgb;
|
||||
break;
|
||||
case 1:
|
||||
o.rgb = vec3(Y);
|
||||
break;
|
||||
case 2:
|
||||
o.rgb = vec3(Y, Cb, Cr);
|
||||
break;
|
||||
case 3:
|
||||
o.rgb = vec3(i.a);
|
||||
break;
|
||||
}
|
||||
|
||||
o_col0 = o;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(ps_stencil_image_init_0) || defined(ps_stencil_image_init_1)
|
||||
|
||||
void main()
|
||||
{
|
||||
o_col0 = vec4(0x7FFFFFFF);
|
||||
|
||||
#ifdef ps_stencil_image_init_0
|
||||
if((127.5f / 255.0f) < sample_c(v_tex).a) // < 0x80 pass (== 0x80 should not pass)
|
||||
o_col0 = vec4(-1);
|
||||
#endif
|
||||
#ifdef ps_stencil_image_init_1
|
||||
if(sample_c(v_tex).a < (127.5f / 255.0f)) // >= 0x80 pass
|
||||
o_col0 = vec4(-1);
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,228 @@
|
||||
#ifdef VERTEX_SHADER
|
||||
|
||||
layout(location = 0) in vec4 a_pos;
|
||||
layout(location = 1) in vec2 a_tex;
|
||||
|
||||
layout(location = 0) out vec2 v_tex;
|
||||
|
||||
void main()
|
||||
{
|
||||
gl_Position = vec4(a_pos.x, -a_pos.y, a_pos.z, a_pos.w);
|
||||
v_tex = a_tex;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
#ifdef FRAGMENT_SHADER
|
||||
|
||||
layout(location = 0) in vec2 v_tex;
|
||||
layout(location = 0) out vec4 o_col0;
|
||||
|
||||
layout(push_constant) uniform cb0
|
||||
{
|
||||
vec4 ZrH;
|
||||
};
|
||||
|
||||
layout(set = 0, binding = 0) uniform sampler2D samp0;
|
||||
|
||||
|
||||
// Weave shader
|
||||
#ifdef ps_main0
|
||||
void ps_main0()
|
||||
{
|
||||
const int idx = int(ZrH.x); // buffer index passed from CPU
|
||||
const int field = idx & 1; // current field
|
||||
const int vpos = int(gl_FragCoord.y); // vertical position of destination texture
|
||||
|
||||
if ((vpos & 1) == field)
|
||||
o_col0 = textureLod(samp0, v_tex, 0);
|
||||
else
|
||||
discard;
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
// Bob shader
|
||||
#ifdef ps_main1
|
||||
void ps_main1()
|
||||
{
|
||||
o_col0 = textureLod(samp0, v_tex, 0);
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
// Blend shader
|
||||
#ifdef ps_main2
|
||||
void ps_main2()
|
||||
{
|
||||
vec2 vstep = vec2(0.0f, ZrH.y);
|
||||
vec4 c0 = textureLod(samp0, v_tex - vstep, 0);
|
||||
vec4 c1 = textureLod(samp0, v_tex, 0);
|
||||
vec4 c2 = textureLod(samp0, v_tex + vstep, 0);
|
||||
|
||||
o_col0 = (c0 + c1 * 2.0f + c2) / 4.0f;
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
// MAD shader - buffering
|
||||
#ifdef ps_main3
|
||||
void ps_main3()
|
||||
{
|
||||
// We take half the lines from the current frame and stores them in the MAD frame buffer.
|
||||
// the MAD frame buffer is split in 2 consecutive banks of 2 fields each, the fields in each bank
|
||||
// are interleaved (top field at even lines and bottom field at odd lines).
|
||||
// When the source texture has an odd vres, the first line of bank 1 would be an odd index
|
||||
// causing the wrong lines to be discarded, so a vertical offset (lofs) is added to the vertical
|
||||
// position of the destination texture to force the proper field alignment
|
||||
|
||||
const int idx = int(ZrH.x); // buffer index passed from CPU
|
||||
const int bank = idx >> 1; // current bank
|
||||
const int field = idx & 1; // current field
|
||||
const int vres = int(ZrH.z) >> 1; // vertical resolution of source texture
|
||||
const int lofs = ((((vres + 1) >> 1) << 1) - vres) & bank; // line alignment offset for bank 1
|
||||
const int vpos = int(gl_FragCoord.y) + lofs; // vertical position of destination texture
|
||||
const vec2 bofs = vec2(0.0f, 0.5f * bank); // vertical offset of the current bank relative to source texture size
|
||||
const vec2 vscale = vec2(1.0f, 2.0f); // scaling factor from source to destination texture
|
||||
const vec2 optr = v_tex - bofs; // used to check if the current destination line is within the current bank
|
||||
const vec2 iptr = optr * vscale; // pointer to the current pixel in the source texture
|
||||
|
||||
// if the index of current destination line belongs to the current fiels we update it, otherwise
|
||||
// we leave the old line in the destination buffer
|
||||
if ((optr.y >= 0.0f) && (optr.y < 0.5f) && ((vpos & 1) == field))
|
||||
o_col0 = textureLod(samp0, iptr, 0);
|
||||
else
|
||||
discard;
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
// MAD shader - reconstruction
|
||||
#ifdef ps_main4
|
||||
void ps_main4()
|
||||
{
|
||||
// we use the contents of the MAD frame buffer to reconstruct the missing lines from the current
|
||||
// field.
|
||||
|
||||
const int idx = int(ZrH.x); // buffer index passed from CPU
|
||||
const int bank = idx >> 1; // current bank
|
||||
const int field = idx & 1; // current field
|
||||
const int vpos = int(gl_FragCoord.y); // vertical position of destination texture
|
||||
const float sensitivity = ZrH.w; // passed from CPU, higher values mean more likely to use weave
|
||||
const vec3 motion_thr = vec3(1.0, 1.0, 1.0) * sensitivity; //
|
||||
const vec2 bofs = vec2(0.0f, 0.5f); // position of the bank 1 relative to source texture size
|
||||
const vec2 vscale = vec2(1.0f, 0.5f); // scaling factor from source to destination texture
|
||||
const vec2 lofs = vec2(0.0f, ZrH.y) * vscale; // distance between two adjacent lines relative to source texture size
|
||||
const vec2 iptr = v_tex * vscale; // pointer to the current pixel in the source texture
|
||||
|
||||
vec2 p_t0; // pointer to current pixel (missing or not) from most recent frame
|
||||
vec2 p_t1; // pointer to current pixel (missing or not) from one frame back
|
||||
vec2 p_t2; // pointer to current pixel (missing or not) from two frames back
|
||||
vec2 p_t3; // pointer to current pixel (missing or not) from three frames back
|
||||
|
||||
switch (idx)
|
||||
{
|
||||
case 1:
|
||||
p_t0 = iptr;
|
||||
p_t1 = iptr;
|
||||
p_t2 = iptr + bofs;
|
||||
p_t3 = iptr + bofs;
|
||||
break;
|
||||
case 2:
|
||||
p_t0 = iptr + bofs;
|
||||
p_t1 = iptr;
|
||||
p_t2 = iptr;
|
||||
p_t3 = iptr + bofs;
|
||||
break;
|
||||
case 3:
|
||||
p_t0 = iptr + bofs;
|
||||
p_t1 = iptr + bofs;
|
||||
p_t2 = iptr;
|
||||
p_t3 = iptr;
|
||||
break;
|
||||
default:
|
||||
p_t0 = iptr;
|
||||
p_t1 = iptr + bofs;
|
||||
p_t2 = iptr + bofs;
|
||||
p_t3 = iptr;
|
||||
break;
|
||||
}
|
||||
|
||||
// calculating motion, only relevant for missing lines where the "center line" is pointed by p_t1
|
||||
|
||||
vec4 hn = textureLod(samp0, p_t0 - lofs, 0); // new high pixel
|
||||
vec4 cn = textureLod(samp0, p_t1, 0); // new center pixel
|
||||
vec4 ln = textureLod(samp0, p_t0 + lofs, 0); // new low pixel
|
||||
|
||||
vec4 ho = textureLod(samp0, p_t2 - lofs, 0); // old high pixel
|
||||
vec4 co = textureLod(samp0, p_t3, 0); // old center pixel
|
||||
vec4 lo = textureLod(samp0, p_t2 + lofs, 0); // old low pixel
|
||||
|
||||
vec3 mh = hn.rgb - ho.rgb; // high pixel motion
|
||||
vec3 mc = cn.rgb - co.rgb; // center pixel motion
|
||||
vec3 ml = ln.rgb - lo.rgb; // low pixel motion
|
||||
|
||||
mh = max(mh, -mh) - motion_thr;
|
||||
mc = max(mc, -mc) - motion_thr;
|
||||
ml = max(ml, -ml) - motion_thr;
|
||||
|
||||
#if 1 // use this code to evaluate each color motion separately
|
||||
float mh_max = max(max(mh.x, mh.y), mh.z);
|
||||
float mc_max = max(max(mc.x, mc.y), mc.z);
|
||||
float ml_max = max(max(ml.x, ml.y), ml.z);
|
||||
#else // use this code to evaluate average color motion
|
||||
float mh_max = mh.x + mh.y + mh.z;
|
||||
float mc_max = mc.x + mc.y + mc.z;
|
||||
float ml_max = ml.x + ml.y + ml.z;
|
||||
#endif
|
||||
|
||||
// selecting deinterlacing output
|
||||
|
||||
if ((vpos & 1) == field) // output coordinate present on current field
|
||||
{
|
||||
// output coordinate present on current field
|
||||
o_col0 = textureLod(samp0, p_t0, 0);
|
||||
}
|
||||
else if ((iptr.y > 0.5f - lofs.y) || (iptr.y < 0.0 + lofs.y))
|
||||
{
|
||||
// top and bottom lines are always weaved
|
||||
o_col0 = cn;
|
||||
}
|
||||
else
|
||||
{
|
||||
// missing line needs to be reconstructed
|
||||
if(((mh_max > 0.0f) || (ml_max > 0.0f)) || (mc_max > 0.0f))
|
||||
// high motion -> interpolate pixels above and below
|
||||
o_col0 = (hn + ln) / 2.0f;
|
||||
else
|
||||
{
|
||||
// Check if it's completely static first, we don't need to mess with any of that.
|
||||
if((mh_max != -motion_thr.x) || (ml_max != -motion_thr.x) || (mc_max != -motion_thr.x))
|
||||
{
|
||||
// Check the diff with the above and below lines, if the difference is smaller between the new high and low lines
|
||||
// compared to the new centre line and the high line (with some threshold of about 25 color steps), then reconstruct.
|
||||
vec3 mhln = hn.rgb - ln.rgb;
|
||||
vec3 mchn = hn.rgb - cn.rgb;
|
||||
|
||||
mhln = max(mhln, -mhln) - motion_thr;
|
||||
mchn = max(mchn, -mchn) - motion_thr;
|
||||
|
||||
float mhln_max = max(max(mhln.x, mhln.y), mhln.z);
|
||||
float mchn_max = max(max(mchn.x, mchn.y), mchn.z);
|
||||
|
||||
// The new centre line is a fair chunk different from those surrounding it, so quite likely incorrect.
|
||||
if (mhln_max < 0.0f && mchn_max >= (mhln_max * 0.90f))
|
||||
o_col0 = (hn + ln) / 2.0f;
|
||||
else
|
||||
// low motion -> weave
|
||||
o_col0 = cn;
|
||||
}
|
||||
else
|
||||
// low motion -> weave
|
||||
o_col0 = cn;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
File diff suppressed because it is too large.
Load diff
File renamed without changes.
+162
-82
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"generated_at": "2026-10-05T00:24:12Z",
|
||||
"total_files": 18983,
|
||||
"total_size": 17478085472,
|
||||
"generated_at": "2026-10-05T22:37:20Z",
|
||||
"total_files": 18988,
|
||||
"total_size": 17479984879,
|
||||
"files": {
|
||||
"738c7f5fd0c8b57ee2e87ae7a97bf8e21a821d07": {
|
||||
"path": "bios/3D Realms/Duke Nukem 3D/DUKE.RTS",
|
||||
@@ -125316,6 +125316,23 @@
|
||||
"crc32": "6337409f",
|
||||
"adler32": "66d3f4d2"
|
||||
},
|
||||
"84b90f69671d4b72e8f219e1fe7cd667e976cf7f": {
|
||||
"path": "bios/NEC/PC-88/font.rom",
|
||||
"name": "font.rom",
|
||||
"size": 2048,
|
||||
"sha1": "84b90f69671d4b72e8f219e1fe7cd667e976cf7f",
|
||||
"md5": "cd428f9ee8ff9f84c60beb7a8a0ef628",
|
||||
"sha256": "3227087599afd9304f260cdeb3bf1533e39e425cf444ceb3c24f55f561e71a0c",
|
||||
"crc32": "56653188",
|
||||
"adler32": "cc606bcd",
|
||||
"provenance": {
|
||||
"tosec": {
|
||||
"dat": "NEC PC-8001 - Firmware",
|
||||
"name": "NEC PC-8001 BIOS (1979)(NEC)[Font ROM].bin",
|
||||
"description": "NEC PC-8001 BIOS (1979)(NEC)[Font ROM]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"0a056b6d28b4b9e29a2d255bdaa25c24f176cf1e": {
|
||||
"path": "bios/NEC/PC-88/font2.rom",
|
||||
"name": "font2.rom",
|
||||
@@ -132912,16 +132929,6 @@
|
||||
"crc32": "2c7a1b2a",
|
||||
"adler32": "974da60e"
|
||||
},
|
||||
"c54f04b63ac6f04e0a0944ef260b5497e4494dbd": {
|
||||
"path": "bios/Other/armsx2/message.wav",
|
||||
"name": "message.wav",
|
||||
"size": 15696,
|
||||
"sha1": "c54f04b63ac6f04e0a0944ef260b5497e4494dbd",
|
||||
"md5": "d66b0bccd743bd11eaca55bcaec925da",
|
||||
"sha256": "dfd8035387a463722ad07af3c91934e0a685f2afa57e31d305f0602656d1edb1",
|
||||
"crc32": "d766f908",
|
||||
"adler32": "9a707155"
|
||||
},
|
||||
"7b0bb79d38c573648de218645c6fb16449e156e0": {
|
||||
"path": "bios/Other/armsx2/no-save.png",
|
||||
"name": "no-save.png",
|
||||
@@ -140726,6 +140733,46 @@
|
||||
"crc32": "c4c366fe",
|
||||
"adler32": "97ab252c"
|
||||
},
|
||||
"fca26d4f7c1fe0abf6534b470da2ee5ad461eba5": {
|
||||
"path": "bios/Other/nethersx2-turnip/assets/GameIndex.yaml",
|
||||
"name": "GameIndex.yaml",
|
||||
"size": 1850565,
|
||||
"sha1": "fca26d4f7c1fe0abf6534b470da2ee5ad461eba5",
|
||||
"md5": "26ab10d8de93c9b9806fa7602faa2b62",
|
||||
"sha256": "b9b4c9fdba361f2af23a5bcc4b69acfda24e056a513197d3b0c78756d0f93ca2",
|
||||
"crc32": "6a148c8c",
|
||||
"adler32": "c9ec943c"
|
||||
},
|
||||
"aedc25c370bf3fe28923dc8359111a098293a503": {
|
||||
"path": "bios/Other/nethersx2-turnip/assets/shaders/vulkan/convert.glsl",
|
||||
"name": "convert.glsl",
|
||||
"size": 9379,
|
||||
"sha1": "aedc25c370bf3fe28923dc8359111a098293a503",
|
||||
"md5": "f3aaf5697b069d1eb9be66fc1c932786",
|
||||
"sha256": "582cf0ee8d041f57ed61d3ebc65a6f9d50769b51a5e539edb09f7df7a69bbf4e",
|
||||
"crc32": "7d421ade",
|
||||
"adler32": "ae373fae"
|
||||
},
|
||||
"5a433c891e071c3885af7a5f7e47fed7e5ec30cf": {
|
||||
"path": "bios/Other/nethersx2-turnip/assets/shaders/vulkan/interlace.glsl",
|
||||
"name": "interlace.glsl",
|
||||
"size": 7745,
|
||||
"sha1": "5a433c891e071c3885af7a5f7e47fed7e5ec30cf",
|
||||
"md5": "08bc6e44a6d86ccad4b0673b445bd9d8",
|
||||
"sha256": "9fad3144dfe573f77aef727fbe25bc9daca44680acc4c222d2f68830a4629cb1",
|
||||
"crc32": "bbd56839",
|
||||
"adler32": "8fb12db1"
|
||||
},
|
||||
"1c3a966a42cefcb9c26ce2fa77e4cd90f2d50329": {
|
||||
"path": "bios/Other/nethersx2-turnip/assets/shaders/vulkan/tfx.glsl",
|
||||
"name": "tfx.glsl",
|
||||
"size": 29670,
|
||||
"sha1": "1c3a966a42cefcb9c26ce2fa77e4cd90f2d50329",
|
||||
"md5": "df60c4e6c5690dd2d3a0cc740800b284",
|
||||
"sha256": "66dac83be1f6b9dd83f5a34db3ed7d2a9f4a92112e86dae1fdb2bb40e1d35c98",
|
||||
"crc32": "0a422746",
|
||||
"adler32": "7435f695"
|
||||
},
|
||||
"58fb1f8ea6cf2d526904349640453e682fe388a1": {
|
||||
"path": "bios/Other/nethersx2-turnip-classic/assets/GameIndex.yaml",
|
||||
"name": "GameIndex.yaml",
|
||||
@@ -192704,6 +192751,16 @@
|
||||
"crc32": "a3e573a0",
|
||||
"adler32": "e808396a"
|
||||
},
|
||||
"c54f04b63ac6f04e0a0944ef260b5497e4494dbd": {
|
||||
"path": "bios/Sony/PlayStation 2/resources/sounds/achievements/message.wav",
|
||||
"name": "message.wav",
|
||||
"size": 15696,
|
||||
"sha1": "c54f04b63ac6f04e0a0944ef260b5497e4494dbd",
|
||||
"md5": "d66b0bccd743bd11eaca55bcaec925da",
|
||||
"sha256": "dfd8035387a463722ad07af3c91934e0a685f2afa57e31d305f0602656d1edb1",
|
||||
"crc32": "d766f908",
|
||||
"adler32": "9a707155"
|
||||
},
|
||||
"47d2ec4b342649e4c391043ab915d4435f9d180d": {
|
||||
"path": "bios/Sony/PlayStation 2/rom1.bin",
|
||||
"name": "rom1.bin",
|
||||
@@ -207327,6 +207384,7 @@
|
||||
"9e5edcd9c670533791034ba84afb4d73": "88ad52740cd0b8a0e927abbfb84601aa099bcfbc",
|
||||
"d81c6d5d7ad1a4bbbd6ae22a01257603": "82e11a177af6a5091dd67f50a2f4bafda84d6556",
|
||||
"f1ce4d5f83717093982e5f75516b8f3c": "7a6676e352c8d0eaf16e5863c1c98ac65d6f8686",
|
||||
"cd428f9ee8ff9f84c60beb7a8a0ef628": "84b90f69671d4b72e8f219e1fe7cd667e976cf7f",
|
||||
"1711df0a52f76bd395fdbc8cada17d38": "0a056b6d28b4b9e29a2d255bdaa25c24f176cf1e",
|
||||
"b1f6e4d362ae0b443bcd3803c753c16d": "aff9959b402ae0ead377b87566cf0e7b889e3d9e",
|
||||
"29aad51cd243c8e449d311d14613f0b1": "22fb2cd4412df057762c23dd22729ebcb922fe0e",
|
||||
@@ -208045,7 +208103,6 @@
|
||||
"fa6a7030a9f259ee7300dd0bd49f24f3": "5ad1e110dcf0ffd26405981398892a82e4f3bad1",
|
||||
"083257a65f869a927d8f5a4534f46106": "36cc8f46014a321b7602e52c062ab23d7c256ccc",
|
||||
"95f294d24b5e97ceab96282f1284190f": "8dba66121f3bbc4b2bdf67722ef4c2b6b6b23ee6",
|
||||
"d66b0bccd743bd11eaca55bcaec925da": "c54f04b63ac6f04e0a0944ef260b5497e4494dbd",
|
||||
"6f47c5daf33a759df3395db8ebe62ed1": "7b0bb79d38c573648de218645c6fb16449e156e0",
|
||||
"75527903c6a793772b02807c5343f4c8": "456e74b0805d1931a7786d3d04673a3b02dd4dd3",
|
||||
"46dbe14f577b56ade221acf89631fd7b": "69253a2722f999ba4205f2e679d0b75e3b17109f",
|
||||
@@ -208825,6 +208882,10 @@
|
||||
"366b45b6c4e9ed8e6b7454eebcb5d010": "26b29f57685d8108cf519c4f1d340d43aacbff6e",
|
||||
"f80e78f88892f0b82ad154eee613f537": "aa06cffa086126bb4972674562a5f5c8dc6082f3",
|
||||
"4d035ee86f836bfed422a975cf65c9cc": "eb488a76217629f620e40287567d7e2b30337680",
|
||||
"26ab10d8de93c9b9806fa7602faa2b62": "fca26d4f7c1fe0abf6534b470da2ee5ad461eba5",
|
||||
"f3aaf5697b069d1eb9be66fc1c932786": "aedc25c370bf3fe28923dc8359111a098293a503",
|
||||
"08bc6e44a6d86ccad4b0673b445bd9d8": "5a433c891e071c3885af7a5f7e47fed7e5ec30cf",
|
||||
"df60c4e6c5690dd2d3a0cc740800b284": "1c3a966a42cefcb9c26ce2fa77e4cd90f2d50329",
|
||||
"3f33de7bf8e40e00a05cf27c18a6be0c": "58fb1f8ea6cf2d526904349640453e682fe388a1",
|
||||
"fb2a43dcea094a19db4f4bcdc4ad32eb": "31cbff66380f255d359ff9ee09b6edc06102579a",
|
||||
"cc19970629feb0a25b9b95263194f08f": "1caae7ddf0cbba5f1f6d7404b2309300d913778c",
|
||||
@@ -213760,6 +213821,7 @@
|
||||
"b53d51edc7fc086685e31b811dc32aad": "34a81db03ab617fbfdd7f9b861692dd2ecd57b82",
|
||||
"5dea62f70439682a6cee16ba3823d11e": "6eddec30056cde7c664a0cf508dcad29353a12bb",
|
||||
"de6da198a7359d1200c3eeb6df9c7eda": "40ecf6138c99a0aba775ef93240b295025a45500",
|
||||
"d66b0bccd743bd11eaca55bcaec925da": "c54f04b63ac6f04e0a0944ef260b5497e4494dbd",
|
||||
"44552702b05697a14ccbe2ca22ee7139": "47d2ec4b342649e4c391043ab915d4435f9d180d",
|
||||
"362e2dac5e4de13591c67fb5eceeb94a": "92ce9e739dcbfa0d6afb442620b71919d26bf283",
|
||||
"93a361b9e10519e5a9aa2da378340e10": "0195572b9ad3be15bd0c47b906b2c14826cd681c",
|
||||
@@ -245562,6 +245624,11 @@
|
||||
"PC8801 MKII SR BIOS.rom": [
|
||||
"7a6676e352c8d0eaf16e5863c1c98ac65d6f8686"
|
||||
],
|
||||
"font.rom": [
|
||||
"84b90f69671d4b72e8f219e1fe7cd667e976cf7f",
|
||||
"425a896af83386c7bafac59f94211cdbb9cce18d",
|
||||
"78ba9960f135372825ab7244b5e4e73a810002ff"
|
||||
],
|
||||
"font2.rom": [
|
||||
"0a056b6d28b4b9e29a2d255bdaa25c24f176cf1e"
|
||||
],
|
||||
@@ -245628,10 +245695,6 @@
|
||||
"SOUND.ROM": [
|
||||
"34137c287c39c44300b04ee97c1e6459bb826b60"
|
||||
],
|
||||
"font.rom": [
|
||||
"425a896af83386c7bafac59f94211cdbb9cce18d",
|
||||
"78ba9960f135372825ab7244b5e4e73a810002ff"
|
||||
],
|
||||
"itf.rom": [
|
||||
"cf3aa193df2722899066246bccbed03f2e79a74a",
|
||||
"a2fb11c000ed7c976520622cfb7940ed6ddc904e"
|
||||
@@ -247564,6 +247627,7 @@
|
||||
"705d20d39a910d58a070fb86780ac58c8007591c",
|
||||
"b2a059787ee297654b27f633e37382e239693a36",
|
||||
"b18a236b62e11a85c7ffda6ae60bfb312d6524ef",
|
||||
"aedc25c370bf3fe28923dc8359111a098293a503",
|
||||
"26a17de3b4abc30e5c19987efad4ceac22e9a369",
|
||||
"7a133d4cf115dc49cd1921bd7049ea64ad58acce",
|
||||
"d26608762bfca4a470facc15c378b901ef27e249",
|
||||
@@ -247582,6 +247646,7 @@
|
||||
"268f43cd946eea999c62f63a24eb00cb2fdc342c",
|
||||
"e74e58f530abb4af02c0875c2f04d507e854869e",
|
||||
"f8d14d80036f11707c7e94db2a77531d582019dd",
|
||||
"5a433c891e071c3885af7a5f7e47fed7e5ec30cf",
|
||||
"60d6562c205ed9179ab092c37568c06b64b00ee5"
|
||||
],
|
||||
"merge.glsl": [
|
||||
@@ -247648,6 +247713,7 @@
|
||||
"tfx.glsl": [
|
||||
"229d91d96b6dc338421e696fe8cdcfcaa093f40d",
|
||||
"aa06cffa086126bb4972674562a5f5c8dc6082f3",
|
||||
"1c3a966a42cefcb9c26ce2fa77e4cd90f2d50329",
|
||||
"b4ebf3f7f20fc88f91cda033552a7489a9ff179c",
|
||||
"0a16f66d7816d73af3b28dea2bb292415695f6f7"
|
||||
],
|
||||
@@ -249446,6 +249512,7 @@
|
||||
],
|
||||
"GameIndex.yaml": [
|
||||
"997fdfb1c1873c1a43972b2984ac0da61fd47b14",
|
||||
"fca26d4f7c1fe0abf6534b470da2ee5ad461eba5",
|
||||
"58fb1f8ea6cf2d526904349640453e682fe388a1",
|
||||
"7efb2ed721cf0c0f0677324e09be311a8337e793",
|
||||
"57369df9ca82a460e596cea7835c5a0797dee668",
|
||||
@@ -267580,9 +267647,6 @@
|
||||
"SCPH-70004_BIOS_V12_EUR_200.ROM2": [
|
||||
"1bae895fbdd658cfb56c53cc2139282cc1e778de"
|
||||
],
|
||||
"ROM2_ST.BIN": [
|
||||
"1bae895fbdd658cfb56c53cc2139282cc1e778de"
|
||||
],
|
||||
"SCPH-90006_BIOS_VX_HK _230.ROM2": [
|
||||
"b96cf94772f01b8434038285896467884a84b0c2"
|
||||
],
|
||||
@@ -269576,12 +269640,6 @@
|
||||
"dmg0_rom.bin": [
|
||||
"1db57a1e8b6e4096f811587f9eab0c6675fd9755"
|
||||
],
|
||||
"cgb0_boot.bin": [
|
||||
"80e3ee82416c1a74827a5459fde32f04bf51a1f6"
|
||||
],
|
||||
"cgb_agb_boot.bin": [
|
||||
"80e3ee82416c1a74827a5459fde32f04bf51a1f6"
|
||||
],
|
||||
"keen4.exe": [
|
||||
"2a674822b9b5a3383114e3a3d97a98836e8e6cf1"
|
||||
],
|
||||
@@ -269708,15 +269766,6 @@
|
||||
"wqy_11pt.pcf": [
|
||||
"723b0c61428321b0aca4c43fcd91406807d4f815"
|
||||
],
|
||||
"dragon32": [
|
||||
"f2dab125673e653995a83bf6b793e3390ec7f65a"
|
||||
],
|
||||
"d32rom": [
|
||||
"f2dab125673e653995a83bf6b793e3390ec7f65a"
|
||||
],
|
||||
"Dragon Data Ltd - Dragon 32 - IC17": [
|
||||
"f2dab125673e653995a83bf6b793e3390ec7f65a"
|
||||
],
|
||||
"d200e_rom1": [
|
||||
"628543ee8b47a56df2b2175cfb763c0051517b90"
|
||||
],
|
||||
@@ -269775,14 +269824,7 @@
|
||||
"389df7981873d9e6e46c84c20cd43af0e4226cf8"
|
||||
],
|
||||
"2608_RYM.WAV": [
|
||||
"4ecf900db8d0f69d6154bde376206d727ddddaae",
|
||||
"c65592330c9dd84011151daed52f9aec926b7e56"
|
||||
],
|
||||
"N88_0.ROM": [
|
||||
"d1ae642aed4f0584eeb81ff50180db694e5101d4"
|
||||
],
|
||||
"n88ext0.rom": [
|
||||
"d1ae642aed4f0584eeb81ff50180db694e5101d4"
|
||||
"4ecf900db8d0f69d6154bde376206d727ddddaae"
|
||||
],
|
||||
"N88_1.ROM": [
|
||||
"4ae4d37409ff99411a623da9f6a44192170a854e"
|
||||
@@ -269805,9 +269847,6 @@
|
||||
"n80.rom": [
|
||||
"063609dd518c124a4fc9ba35d1bae35771666a34"
|
||||
],
|
||||
"n88sub.rom": [
|
||||
"892b85cc96f0ff31cae8ad8d409b9f6c04b32e5e"
|
||||
],
|
||||
"KANJI2.ROM": [
|
||||
"b86b932cb4ce34472e0fe0f01a4a4350e15dcb53"
|
||||
],
|
||||
@@ -269823,12 +269862,6 @@
|
||||
"blasphemer.wad": [
|
||||
"9d071480b3e882cce13fc9be627fb6ea2ec0c2eb"
|
||||
],
|
||||
"libopengl1.so": [
|
||||
"5d53b8622056b2cf2e58455a8b7adcbe24caf626"
|
||||
],
|
||||
"libopenglex.so": [
|
||||
"5d53b8622056b2cf2e58455a8b7adcbe24caf626"
|
||||
],
|
||||
"Keyboard.cfg": [
|
||||
"a80c28156b09ff2144d60424733416ca25593870"
|
||||
],
|
||||
@@ -269907,9 +269940,6 @@
|
||||
"jk1mpdemo.gob": [
|
||||
"d2b6982051f2ed8887b824057903c9218fe75485"
|
||||
],
|
||||
"intro.lvl": [
|
||||
"b70182fad8bab129f70a863558ece27121370937"
|
||||
],
|
||||
"outro.lvl": [
|
||||
"6e1728edd9b634e04ecf72e59428eb783e1e545b"
|
||||
],
|
||||
@@ -269922,12 +269952,6 @@
|
||||
"bank0.bin": [
|
||||
"1246203bebf9f04e3bac2df7fc64719304f9f1bd"
|
||||
],
|
||||
"neogeo.7z": [
|
||||
"df89ef91bbaf84f2a109377fd4b17b0050a163e8"
|
||||
],
|
||||
"neogeo.rar": [
|
||||
"df89ef91bbaf84f2a109377fd4b17b0050a163e8"
|
||||
],
|
||||
"DevIL.dll": [
|
||||
"313a23600a169adbe130b1ae1784d8de437ce7d9"
|
||||
],
|
||||
@@ -269943,15 +269967,6 @@
|
||||
"Qsound_V1.94.rom": [
|
||||
"622a81dce98524148f067780c17eddbaf46e27f6"
|
||||
],
|
||||
"dc_flash_wb.bin": [
|
||||
"72c65f2a11ee7b72b77b9945187b7a332632e14a"
|
||||
],
|
||||
"dc_flash.bin.bin": [
|
||||
"72c65f2a11ee7b72b77b9945187b7a332632e14a"
|
||||
],
|
||||
"libnfs.so": [
|
||||
"73fc328ddfaa5cec953107904e006c0d9d0de6dc"
|
||||
],
|
||||
"342-0135-b.64.rom": [
|
||||
"61fa9254628e5bb7236fb474006116d67684d747"
|
||||
],
|
||||
@@ -269982,6 +269997,12 @@
|
||||
"Thunderware_REV_1.3_ROM_U9.bin": [
|
||||
"60f434f5325899d7ea257a6e56e6f53eae65146a"
|
||||
],
|
||||
"cgb0_boot.bin": [
|
||||
"80e3ee82416c1a74827a5459fde32f04bf51a1f6"
|
||||
],
|
||||
"cgb_agb_boot.bin": [
|
||||
"80e3ee82416c1a74827a5459fde32f04bf51a1f6"
|
||||
],
|
||||
"nds_sd_card.bin": [
|
||||
"3b71f43ff30f4b15b5cd85dd9e95ebc7e84eb5a3"
|
||||
],
|
||||
@@ -282325,6 +282346,7 @@
|
||||
"84760465": "88ad52740cd0b8a0e927abbfb84601aa099bcfbc",
|
||||
"6178bd43": "82e11a177af6a5091dd67f50a2f4bafda84d6556",
|
||||
"6337409f": "7a6676e352c8d0eaf16e5863c1c98ac65d6f8686",
|
||||
"56653188": "84b90f69671d4b72e8f219e1fe7cd667e976cf7f",
|
||||
"1a86221f": "0a056b6d28b4b9e29a2d255bdaa25c24f176cf1e",
|
||||
"58c0c76f": "aff9959b402ae0ead377b87566cf0e7b889e3d9e",
|
||||
"fcb60c01": "22fb2cd4412df057762c23dd22729ebcb922fe0e",
|
||||
@@ -283043,7 +283065,6 @@
|
||||
"4d30912d": "5ad1e110dcf0ffd26405981398892a82e4f3bad1",
|
||||
"8216948a": "36cc8f46014a321b7602e52c062ab23d7c256ccc",
|
||||
"2c7a1b2a": "8dba66121f3bbc4b2bdf67722ef4c2b6b6b23ee6",
|
||||
"d766f908": "c54f04b63ac6f04e0a0944ef260b5497e4494dbd",
|
||||
"6e8b2b47": "7b0bb79d38c573648de218645c6fb16449e156e0",
|
||||
"92685e68": "456e74b0805d1931a7786d3d04673a3b02dd4dd3",
|
||||
"b02dadf9": "69253a2722f999ba4205f2e679d0b75e3b17109f",
|
||||
@@ -283823,6 +283844,10 @@
|
||||
"9fbe5028": "26b29f57685d8108cf519c4f1d340d43aacbff6e",
|
||||
"89fe5a62": "aa06cffa086126bb4972674562a5f5c8dc6082f3",
|
||||
"c4c366fe": "eb488a76217629f620e40287567d7e2b30337680",
|
||||
"6a148c8c": "fca26d4f7c1fe0abf6534b470da2ee5ad461eba5",
|
||||
"7d421ade": "aedc25c370bf3fe28923dc8359111a098293a503",
|
||||
"bbd56839": "5a433c891e071c3885af7a5f7e47fed7e5ec30cf",
|
||||
"0a422746": "1c3a966a42cefcb9c26ce2fa77e4cd90f2d50329",
|
||||
"7c34b276": "58fb1f8ea6cf2d526904349640453e682fe388a1",
|
||||
"53325491": "31cbff66380f255d359ff9ee09b6edc06102579a",
|
||||
"701c8818": "1caae7ddf0cbba5f1f6d7404b2309300d913778c",
|
||||
@@ -288758,6 +288783,7 @@
|
||||
"25f6212a": "34a81db03ab617fbfdd7f9b861692dd2ecd57b82",
|
||||
"0b805686": "6eddec30056cde7c664a0cf508dcad29353a12bb",
|
||||
"a3e573a0": "40ecf6138c99a0aba775ef93240b295025a45500",
|
||||
"d766f908": "c54f04b63ac6f04e0a0944ef260b5497e4494dbd",
|
||||
"2c3bcd32": "47d2ec4b342649e4c391043ab915d4435f9d180d",
|
||||
"0509cea5": "92ce9e739dcbfa0d6afb442620b71919d26bf283",
|
||||
"4e873b8e": "0195572b9ad3be15bd0c47b906b2c14826cd681c",
|
||||
@@ -301310,6 +301336,7 @@
|
||||
"12689b8c462b9a6e56da31630c02d6d8a3d5a4183bfac2cbe47c5a4563fa4724": "88ad52740cd0b8a0e927abbfb84601aa099bcfbc",
|
||||
"7608040cffb1951e5cc567abb63f75b5746777a1ba96196c1b75606b793bb4bb": "82e11a177af6a5091dd67f50a2f4bafda84d6556",
|
||||
"0dc3fcf58c90c1f119a710babd54b57babf81ba8573c070b33f0cc21948b3086": "7a6676e352c8d0eaf16e5863c1c98ac65d6f8686",
|
||||
"3227087599afd9304f260cdeb3bf1533e39e425cf444ceb3c24f55f561e71a0c": "84b90f69671d4b72e8f219e1fe7cd667e976cf7f",
|
||||
"d35d380c8a716c086cdee9f821c879a01196eeec4e52c94a93f79631248c11ba": "0a056b6d28b4b9e29a2d255bdaa25c24f176cf1e",
|
||||
"4c5da1098f71dd9d8f7e1c4852b749766389571dbcddf7526a85f02f35440791": "aff9959b402ae0ead377b87566cf0e7b889e3d9e",
|
||||
"083c383103948dc4f05ac2a4e1bd7f12c02924c7fcd81d98e0ae13f2b309ab2f": "22fb2cd4412df057762c23dd22729ebcb922fe0e",
|
||||
@@ -302028,7 +302055,6 @@
|
||||
"08ca1285218bf04048d2064a20f79e6bf72267fc98d555430502c245d75b42ab": "5ad1e110dcf0ffd26405981398892a82e4f3bad1",
|
||||
"022c8b05d8f3e7e6046077a2d6ad2d3da1e7fa0e69c8321f3c556b9963be2477": "36cc8f46014a321b7602e52c062ab23d7c256ccc",
|
||||
"c749cd9513bd345b9f12ff46eac0ffbf4e54c0b0884921df97ca8297396e3fea": "8dba66121f3bbc4b2bdf67722ef4c2b6b6b23ee6",
|
||||
"dfd8035387a463722ad07af3c91934e0a685f2afa57e31d305f0602656d1edb1": "c54f04b63ac6f04e0a0944ef260b5497e4494dbd",
|
||||
"ab829598e15b771112130cbafbd982736400690c11e3d7c81e3fc8c582c24f47": "7b0bb79d38c573648de218645c6fb16449e156e0",
|
||||
"90f50de40af560ad55f2fcf460af8205f842b534b057bd8b8bcf7d3e10d3223a": "456e74b0805d1931a7786d3d04673a3b02dd4dd3",
|
||||
"11fa517464dbd568e855e449650a9004d1072c4a54ac1845aaafc462c0652b64": "69253a2722f999ba4205f2e679d0b75e3b17109f",
|
||||
@@ -302808,6 +302834,10 @@
|
||||
"6e2bcc05192e730ef1d6f6f9e7fc1006124074e8a65be9033ed0b221c3a90c68": "26b29f57685d8108cf519c4f1d340d43aacbff6e",
|
||||
"660f19006f7defae762bd07cac59d8989b4fde8bef30eb7ef3bb0136d8ef0a12": "aa06cffa086126bb4972674562a5f5c8dc6082f3",
|
||||
"d9641efdb7bf796a7e715be240b1148cb6d25dab23606b999df5dc63cf16e5bc": "eb488a76217629f620e40287567d7e2b30337680",
|
||||
"b9b4c9fdba361f2af23a5bcc4b69acfda24e056a513197d3b0c78756d0f93ca2": "fca26d4f7c1fe0abf6534b470da2ee5ad461eba5",
|
||||
"582cf0ee8d041f57ed61d3ebc65a6f9d50769b51a5e539edb09f7df7a69bbf4e": "aedc25c370bf3fe28923dc8359111a098293a503",
|
||||
"9fad3144dfe573f77aef727fbe25bc9daca44680acc4c222d2f68830a4629cb1": "5a433c891e071c3885af7a5f7e47fed7e5ec30cf",
|
||||
"66dac83be1f6b9dd83f5a34db3ed7d2a9f4a92112e86dae1fdb2bb40e1d35c98": "1c3a966a42cefcb9c26ce2fa77e4cd90f2d50329",
|
||||
"036a2eb06bbddaeb654e8d82881e385110671ee77003dc79f9acd0ec477bcbf7": "58fb1f8ea6cf2d526904349640453e682fe388a1",
|
||||
"dfe5bf3c70133a320fcbe1670b5de08492bd966db2ea383c0388b3df105937d7": "31cbff66380f255d359ff9ee09b6edc06102579a",
|
||||
"3dd0d5abcf09ac4c0f8419b410b21d11e824af12f9b7df860a668d59156bcb87": "1caae7ddf0cbba5f1f6d7404b2309300d913778c",
|
||||
@@ -307743,6 +307773,7 @@
|
||||
"e8eed9831c39fc298be087ba11d620673134bc683b97ccc909c555d29cc1b207": "34a81db03ab617fbfdd7f9b861692dd2ecd57b82",
|
||||
"3e14a0d3db871704a9652a5d90d4ba62be2bceefbca6bb46da65275b09d4f78e": "6eddec30056cde7c664a0cf508dcad29353a12bb",
|
||||
"483face8f50f5e96d89528059522be3766b44f5e07e854bae1fa92ff0111d0a7": "40ecf6138c99a0aba775ef93240b295025a45500",
|
||||
"dfd8035387a463722ad07af3c91934e0a685f2afa57e31d305f0602656d1edb1": "c54f04b63ac6f04e0a0944ef260b5497e4494dbd",
|
||||
"41de2047af8382988bfd568035ff26eec5f1cabc3efe773680546aa00a82857d": "47d2ec4b342649e4c391043ab915d4435f9d180d",
|
||||
"fc1b5e12e426b229fc4f114a4b8cbe6a5441f7e0ce6654abce382e677f1c051d": "92ce9e739dcbfa0d6afb442620b71919d26bf283",
|
||||
"c4527ec1c2a3aafc6a6b31d3c455d352f28a21ea905ba7df7d7247c9cb4b9e9b": "0195572b9ad3be15bd0c47b906b2c14826cd681c",
|
||||
@@ -461754,6 +461785,12 @@
|
||||
"PC-88/PC8801 MKII SR BIOS.rom": [
|
||||
"7a6676e352c8d0eaf16e5863c1c98ac65d6f8686"
|
||||
],
|
||||
"NEC/PC-88/font.rom": [
|
||||
"84b90f69671d4b72e8f219e1fe7cd667e976cf7f"
|
||||
],
|
||||
"PC-88/font.rom": [
|
||||
"84b90f69671d4b72e8f219e1fe7cd667e976cf7f"
|
||||
],
|
||||
"NEC/PC-88/font2.rom": [
|
||||
"0a056b6d28b4b9e29a2d255bdaa25c24f176cf1e"
|
||||
],
|
||||
@@ -469327,12 +469364,6 @@
|
||||
"armsx2/merge.fx": [
|
||||
"8dba66121f3bbc4b2bdf67722ef4c2b6b6b23ee6"
|
||||
],
|
||||
"Other/armsx2/message.wav": [
|
||||
"c54f04b63ac6f04e0a0944ef260b5497e4494dbd"
|
||||
],
|
||||
"armsx2/message.wav": [
|
||||
"c54f04b63ac6f04e0a0944ef260b5497e4494dbd"
|
||||
],
|
||||
"Other/armsx2/no-save.png": [
|
||||
"7b0bb79d38c573648de218645c6fb16449e156e0"
|
||||
],
|
||||
@@ -469673,6 +469704,7 @@
|
||||
"53ff273e335432fdbbff9da2762e229ec2bd5078",
|
||||
"705d20d39a910d58a070fb86780ac58c8007591c",
|
||||
"b18a236b62e11a85c7ffda6ae60bfb312d6524ef",
|
||||
"aedc25c370bf3fe28923dc8359111a098293a503",
|
||||
"7a133d4cf115dc49cd1921bd7049ea64ad58acce",
|
||||
"0cb56397052613612faf8d5eedfc77066b1fea16"
|
||||
],
|
||||
@@ -469680,6 +469712,7 @@
|
||||
"53ff273e335432fdbbff9da2762e229ec2bd5078",
|
||||
"705d20d39a910d58a070fb86780ac58c8007591c",
|
||||
"b18a236b62e11a85c7ffda6ae60bfb312d6524ef",
|
||||
"aedc25c370bf3fe28923dc8359111a098293a503",
|
||||
"7a133d4cf115dc49cd1921bd7049ea64ad58acce",
|
||||
"0cb56397052613612faf8d5eedfc77066b1fea16"
|
||||
],
|
||||
@@ -469738,12 +469771,14 @@
|
||||
"shaders/vulkan/interlace.glsl": [
|
||||
"d5ba24ba836c418f7f2d9d8b44bd337baa9e45a3",
|
||||
"268f43cd946eea999c62f63a24eb00cb2fdc342c",
|
||||
"f8d14d80036f11707c7e94db2a77531d582019dd"
|
||||
"f8d14d80036f11707c7e94db2a77531d582019dd",
|
||||
"5a433c891e071c3885af7a5f7e47fed7e5ec30cf"
|
||||
],
|
||||
"vulkan/interlace.glsl": [
|
||||
"d5ba24ba836c418f7f2d9d8b44bd337baa9e45a3",
|
||||
"268f43cd946eea999c62f63a24eb00cb2fdc342c",
|
||||
"f8d14d80036f11707c7e94db2a77531d582019dd"
|
||||
"f8d14d80036f11707c7e94db2a77531d582019dd",
|
||||
"5a433c891e071c3885af7a5f7e47fed7e5ec30cf"
|
||||
],
|
||||
"Other/armsx2/pcsx2/resources/shaders/vulkan/merge.glsl": [
|
||||
"e84aa516874350f414e95d3891ce480fe2958aae"
|
||||
@@ -475770,6 +475805,7 @@
|
||||
],
|
||||
"assets/GameIndex.yaml": [
|
||||
"997fdfb1c1873c1a43972b2984ac0da61fd47b14",
|
||||
"fca26d4f7c1fe0abf6534b470da2ee5ad461eba5",
|
||||
"58fb1f8ea6cf2d526904349640453e682fe388a1"
|
||||
],
|
||||
"Other/nethersx2/assets/cheats_ni.zip": [
|
||||
@@ -476472,6 +476508,7 @@
|
||||
],
|
||||
"assets/shaders/vulkan/convert.glsl": [
|
||||
"b18a236b62e11a85c7ffda6ae60bfb312d6524ef",
|
||||
"aedc25c370bf3fe28923dc8359111a098293a503",
|
||||
"7a133d4cf115dc49cd1921bd7049ea64ad58acce"
|
||||
],
|
||||
"Other/nethersx2/assets/shaders/vulkan/interlace.glsl": [
|
||||
@@ -476481,7 +476518,8 @@
|
||||
"f8d14d80036f11707c7e94db2a77531d582019dd"
|
||||
],
|
||||
"assets/shaders/vulkan/interlace.glsl": [
|
||||
"f8d14d80036f11707c7e94db2a77531d582019dd"
|
||||
"f8d14d80036f11707c7e94db2a77531d582019dd",
|
||||
"5a433c891e071c3885af7a5f7e47fed7e5ec30cf"
|
||||
],
|
||||
"Other/nethersx2/assets/shaders/vulkan/merge.glsl": [
|
||||
"b09de39d6459c70ffab28dcb3e3ef879b1648674"
|
||||
@@ -476518,15 +476556,18 @@
|
||||
],
|
||||
"assets/shaders/vulkan/tfx.glsl": [
|
||||
"aa06cffa086126bb4972674562a5f5c8dc6082f3",
|
||||
"1c3a966a42cefcb9c26ce2fa77e4cd90f2d50329",
|
||||
"b4ebf3f7f20fc88f91cda033552a7489a9ff179c"
|
||||
],
|
||||
"shaders/vulkan/tfx.glsl": [
|
||||
"aa06cffa086126bb4972674562a5f5c8dc6082f3",
|
||||
"1c3a966a42cefcb9c26ce2fa77e4cd90f2d50329",
|
||||
"b4ebf3f7f20fc88f91cda033552a7489a9ff179c",
|
||||
"0a16f66d7816d73af3b28dea2bb292415695f6f7"
|
||||
],
|
||||
"vulkan/tfx.glsl": [
|
||||
"aa06cffa086126bb4972674562a5f5c8dc6082f3",
|
||||
"1c3a966a42cefcb9c26ce2fa77e4cd90f2d50329",
|
||||
"b4ebf3f7f20fc88f91cda033552a7489a9ff179c",
|
||||
"0a16f66d7816d73af3b28dea2bb292415695f6f7"
|
||||
],
|
||||
@@ -476545,6 +476586,30 @@
|
||||
"achievements/unlock.wav": [
|
||||
"eb488a76217629f620e40287567d7e2b30337680"
|
||||
],
|
||||
"Other/nethersx2-turnip/assets/GameIndex.yaml": [
|
||||
"fca26d4f7c1fe0abf6534b470da2ee5ad461eba5"
|
||||
],
|
||||
"nethersx2-turnip/assets/GameIndex.yaml": [
|
||||
"fca26d4f7c1fe0abf6534b470da2ee5ad461eba5"
|
||||
],
|
||||
"Other/nethersx2-turnip/assets/shaders/vulkan/convert.glsl": [
|
||||
"aedc25c370bf3fe28923dc8359111a098293a503"
|
||||
],
|
||||
"nethersx2-turnip/assets/shaders/vulkan/convert.glsl": [
|
||||
"aedc25c370bf3fe28923dc8359111a098293a503"
|
||||
],
|
||||
"Other/nethersx2-turnip/assets/shaders/vulkan/interlace.glsl": [
|
||||
"5a433c891e071c3885af7a5f7e47fed7e5ec30cf"
|
||||
],
|
||||
"nethersx2-turnip/assets/shaders/vulkan/interlace.glsl": [
|
||||
"5a433c891e071c3885af7a5f7e47fed7e5ec30cf"
|
||||
],
|
||||
"Other/nethersx2-turnip/assets/shaders/vulkan/tfx.glsl": [
|
||||
"1c3a966a42cefcb9c26ce2fa77e4cd90f2d50329"
|
||||
],
|
||||
"nethersx2-turnip/assets/shaders/vulkan/tfx.glsl": [
|
||||
"1c3a966a42cefcb9c26ce2fa77e4cd90f2d50329"
|
||||
],
|
||||
"Other/nethersx2-turnip-classic/assets/GameIndex.yaml": [
|
||||
"58fb1f8ea6cf2d526904349640453e682fe388a1"
|
||||
],
|
||||
@@ -543292,6 +543357,21 @@
|
||||
"resources/GameIndex.yaml": [
|
||||
"40ecf6138c99a0aba775ef93240b295025a45500"
|
||||
],
|
||||
"Sony/PlayStation 2/resources/sounds/achievements/message.wav": [
|
||||
"c54f04b63ac6f04e0a0944ef260b5497e4494dbd"
|
||||
],
|
||||
"PlayStation 2/resources/sounds/achievements/message.wav": [
|
||||
"c54f04b63ac6f04e0a0944ef260b5497e4494dbd"
|
||||
],
|
||||
"resources/sounds/achievements/message.wav": [
|
||||
"c54f04b63ac6f04e0a0944ef260b5497e4494dbd"
|
||||
],
|
||||
"sounds/achievements/message.wav": [
|
||||
"c54f04b63ac6f04e0a0944ef260b5497e4494dbd"
|
||||
],
|
||||
"achievements/message.wav": [
|
||||
"c54f04b63ac6f04e0a0944ef260b5497e4494dbd"
|
||||
],
|
||||
"Sony/PlayStation 2/rom1.bin": [
|
||||
"47d2ec4b342649e4c391043ab915d4435f9d180d"
|
||||
],
|
||||
|
||||
+65
-59
@@ -4,10 +4,10 @@ core_classification: community_fork
|
||||
source: "https://github.com/aenu1/aps3e"
|
||||
source_branch: main4
|
||||
upstream: "https://github.com/RPCS3/rpcs3"
|
||||
profiled_date: "2026-08-11"
|
||||
source_commit: "4e579526bda21c2c51fc8512ad408a2004cc1798"
|
||||
upstream_commit: "40e9ee5af0de7ca31691c58eebe64ba205a2900b"
|
||||
core_version: "2.41"
|
||||
profiled_date: "2026-10-06"
|
||||
source_commit: "c6ca18217f2f1563235f83d94df78e35423a1720"
|
||||
upstream_commit: "1f080485b95db2700c5cf244b1314f73b6cf564e"
|
||||
core_version: "2.43"
|
||||
display_name: "Sony - PlayStation 3 (aPS3e)"
|
||||
cores:
|
||||
- aps3e
|
||||
@@ -18,16 +18,16 @@ notes: |
|
||||
PlayStation 3 emulator for Android, distributed on Google Play as aenu.aps3e
|
||||
and aenu.aps3e.premium. RPCS3 is vendored whole under app/src/main/cpp/rpcs3
|
||||
rather than pulled as a submodule, and a JNI layer of about six thousand lines
|
||||
replaces the Qt front end. The vendored tree reports itself as 0.0.41-alpha.1
|
||||
at RPCS3 commit 40e9ee5a.
|
||||
replaces the Qt front end. The vendored tree reports itself as 0.0.42-alpha.1
|
||||
at RPCS3 commit 1f080485.
|
||||
ref: app/src/main/cpp/rpcs3/rpcs3/git-version.h:3,
|
||||
app/src/main/cpp/rpcs3/rpcs3/rpcs3_version.cpp:29
|
||||
|
||||
Source drops land on successive branches, not on the default one. Release 2.41
|
||||
of 2026-07-30 is the commit titled 2.41 at the head of main4; the lightweight
|
||||
tags 2.39, 2.40 and 2.41 all sit on the head of main2, an April tree that
|
||||
predates every one of those releases and whose generated git-version.h was
|
||||
never filled in. main4 is the tree read here. No versionName is usable either:
|
||||
Source drops land on successive branches, not on the default one. Release 2.43
|
||||
of 2026-10-05 is the commit titled 2.43 at the head of main4; the lightweight
|
||||
tags 2.39 to 2.43 all sit on the head of main2, the default branch, an April
|
||||
tree that predates every one of those releases and whose generated
|
||||
git-version.h was never filled in. main4 is the tree read here. No versionName is usable either:
|
||||
the only gradle file committed is app/build.gradle.bak, which carries the
|
||||
placeholder 1.19a and is renamed and completed with the signing block at build
|
||||
time.
|
||||
@@ -39,7 +39,7 @@ notes: |
|
||||
custom font directory sits at the data root, and the custom GPU driver
|
||||
directory sits in internal storage because the external filesystem cannot
|
||||
carry an executable mapping.
|
||||
ref: app/src/main/cpp/aps3e_rp3_impl.cpp:151-157,
|
||||
ref: app/src/main/cpp/aps3e_rp3_impl.cpp:139-145,
|
||||
app/src/main/java/aenu/aps3e/Application.java:21-64
|
||||
|
||||
The PS3 firmware is required and nothing runs without it. Three independent
|
||||
@@ -51,10 +51,10 @@ notes: |
|
||||
EmulatorActivity, which does not pass through the game list. The shipped
|
||||
configuration leaves Libraries Control empty, so liblv2.sprx keeps its default
|
||||
entry in the module list and the check fires.
|
||||
ref: app/src/main/java/aenu/aps3e/MainActivity.java:145-147,
|
||||
app/src/main/java/aenu/aps3e/MainActivity.java:189-192,
|
||||
app/src/main/java/aenu/aps3e/QuickStartActivity.java:318-320,
|
||||
app/src/main/cpp/rpcs3/rpcs3/Emu/System.cpp:2763-2783,
|
||||
ref: app/src/main/java/aenu/aps3e/MainActivity.java:144-146,
|
||||
app/src/main/java/aenu/aps3e/MainActivity.java:188-191,
|
||||
app/src/main/java/aenu/aps3e/QuickStartActivity.java:317-319,
|
||||
app/src/main/cpp/rpcs3/rpcs3/Emu/System.cpp:2829-2849,
|
||||
app/src/main/cpp/rpcs3/rpcs3/Emu/Cell/lv2/sys_prx.cpp:86,
|
||||
aps3e/app/src/main/assets/config/config.yml
|
||||
|
||||
@@ -67,39 +67,40 @@ notes: |
|
||||
come from the firmware rather than from a separate download. Custom points all
|
||||
three classes at one file and reverts to From Firmware when that path no
|
||||
longer exists.
|
||||
ref: app/src/main/cpp/aps3e_rp3_impl.cpp:216-246,
|
||||
app/src/main/cpp/rpcs3/rpcs3/Emu/RSX/Overlays/overlay_fonts.cpp:113-118
|
||||
ref: app/src/main/cpp/aps3e_rp3_impl.cpp:204-234,
|
||||
app/src/main/cpp/rpcs3/rpcs3/Emu/RSX/Overlays/overlay_fonts.cpp:112-117
|
||||
|
||||
Vulkan is reached through a loader chosen before the emulator starts. The
|
||||
global config YAML is read directly for Use Custom Driver and Custom Driver
|
||||
Library Path, and the named library is dlopened when both are set and the path
|
||||
exists; otherwise the system libvulkan.so is used. Drivers are imported either
|
||||
as a bare shared object or as a zip whose meta.json names the library.
|
||||
ref: app/src/main/cpp/aps3e_emu.cpp:1015-1039,
|
||||
app/src/main/cpp/vkapi.cpp:21,
|
||||
app/src/main/java/aenu/aps3e/EmulatorSettings.java:1235-1330
|
||||
Library Path, and the named library is opened through libadrenotools as a
|
||||
custom driver when both are set and the path exists; otherwise the system
|
||||
libvulkan.so is dlopened. Drivers are imported either as a bare shared object
|
||||
or as a zip whose meta.json names the library.
|
||||
ref: app/src/main/cpp/aps3e_emu.cpp:952-976,
|
||||
app/src/main/cpp/vkapi.cpp:18-33,
|
||||
app/src/main/java/aenu/aps3e/EmulatorSettings.java:1232-1327
|
||||
|
||||
Encrypted disc images are supported by a loader that has no counterpart in the
|
||||
desktop code line. Launching an ISO makes the Java layer look for a sidecar key
|
||||
beside it, trying the base name with .dkey and then with .key, and hand the
|
||||
descriptor down beside the image. The key is accepted at exactly 32 bytes, read
|
||||
as a hex string, or exactly 16 bytes, read as raw bytes, and installed as an
|
||||
AES-128 decryption context. A 3k3y watermark at offset 0xF70 is recognised
|
||||
separately and needs no key.
|
||||
ref: app/src/main/java/aenu/aps3e/Utils.java:200-220,
|
||||
app/src/main/java/aenu/aps3e/EmulatorActivity.java:149-155,
|
||||
app/src/main/cpp/rpcs3/rpcs3/Loader/iso.cpp:354-399,
|
||||
app/src/main/cpp/rpcs3/rpcs3/Loader/iso.cpp:508-554
|
||||
Encrypted disc images are decrypted by the ISO loader. Launching an ISO makes
|
||||
the Java layer look for a sidecar key beside it, trying the base name with
|
||||
.dkey and then with .key, and hand the descriptor down beside the image. At
|
||||
most 32 bytes of the key are read: a full 32-byte read is taken as a hex
|
||||
string, a 16-byte file as raw bytes, and the result is installed as an AES-128
|
||||
decryption context. A 3k3y watermark at offset 0xF70 is recognised separately
|
||||
and needs no key.
|
||||
ref: app/src/main/java/aenu/aps3e/Utils.java:185-205,
|
||||
app/src/main/java/aenu/aps3e/EmulatorActivity.java:148-154,
|
||||
app/src/main/cpp/rpcs3/rpcs3/Loader/iso.cpp:362-408,
|
||||
app/src/main/cpp/rpcs3/rpcs3/Loader/iso.cpp:517-562
|
||||
|
||||
One import entry point takes EDAT, RAP and PKG. A RAP is copied verbatim into
|
||||
the exdata folder of user 00000001 and rejected unless it is exactly 16 bytes.
|
||||
An EDAT is parsed for its NPD header, rejected below 0x90 bytes or without the
|
||||
NPD magic, and written to the same folder under the content id the header
|
||||
carries. A PKG goes through the package reader and lands in dev_hdd0.
|
||||
ref: app/src/main/java/aenu/aps3e/MainActivity.java:700-711,
|
||||
app/src/main/java/aenu/aps3e/Emulator.java:820-844,
|
||||
app/src/main/cpp/aps3e_emu.cpp:941-969,
|
||||
app/src/main/cpp/aps3e_emu.cpp:923-933
|
||||
ref: app/src/main/java/aenu/aps3e/MainActivity.java:624-658,
|
||||
app/src/main/java/aenu/aps3e/Emulator.java:820-845,
|
||||
app/src/main/cpp/aps3e_emu.cpp:878-906,
|
||||
app/src/main/cpp/aps3e_emu.cpp:860-870
|
||||
|
||||
patch.yml, compatibility_table.json and database_config.json ship inside the
|
||||
APK as assets and are not user supplied.
|
||||
@@ -119,21 +120,23 @@ files:
|
||||
SCEPKG keys and their inner TARs extracted over dev_flash. The Java layer
|
||||
creates dev_flash/.installed once the native call returns true. No digest
|
||||
of the file itself is compared against anything.
|
||||
source_ref: "app/src/main/cpp/aps3e_emu.cpp:727-897, app/src/main/cpp/emulator_aps3e.cpp:1078, app/src/main/java/aenu/aps3e/MainActivity.java:644-676, app/src/main/res/values/strings.xml:132-134"
|
||||
source_ref: "app/src/main/cpp/aps3e_emu.cpp:664-835, app/src/main/cpp/emulator_aps3e.cpp:1078, app/src/main/java/aenu/aps3e/MainActivity.java:591-623, app/src/main/res/values/strings.xml:120-122"
|
||||
|
||||
- name: "<game>.dkey"
|
||||
aliases: ["<game>.key"]
|
||||
required: false
|
||||
size: [16, 32]
|
||||
validation: [size]
|
||||
size_note: "Read capped at 32 bytes: a file of 32 bytes or more is taken as hex from its first 32 characters, a 16-byte file as raw, any other length is refused."
|
||||
unsourceable: "per-disc AES-128 key named after the image it decrypts, one per title, carried by disc dump catalogues rather than by the emulator"
|
||||
description: "Redump disc decryption key read beside an encrypted ISO"
|
||||
note: >-
|
||||
Resolved as a sidecar of the image being launched, .dkey first then .key,
|
||||
and passed down as a second descriptor. 32 bytes are read as a hex string
|
||||
and converted, 16 bytes are taken as raw; any other length fails the key
|
||||
check. An image whose odd regions are encrypted does not boot without it.
|
||||
source_ref: "app/src/main/java/aenu/aps3e/Utils.java:200-220, app/src/main/cpp/rpcs3/rpcs3/Loader/iso.cpp:354-399, app/src/main/cpp/rpcs3/rpcs3/Loader/iso.cpp:850-870"
|
||||
and passed down as a second descriptor. Up to 32 bytes are read: a full
|
||||
32-byte read is taken as a hex string and converted, a 16-byte file as raw;
|
||||
any other length fails the key check. An image whose odd regions are
|
||||
encrypted does not boot without it.
|
||||
source_ref: "app/src/main/java/aenu/aps3e/Utils.java:185-205, app/src/main/cpp/rpcs3/rpcs3/Loader/iso.cpp:362-408, app/src/main/cpp/rpcs3/rpcs3/Loader/iso.cpp:855-882"
|
||||
|
||||
- name: "<content_id>.rap"
|
||||
required: false
|
||||
@@ -145,8 +148,10 @@ files:
|
||||
Imported through the EDAT/RAP/PKG entry and copied byte for byte into
|
||||
dev_hdd0/home/00000001/exdata under the name it was given. The import is
|
||||
refused unless the source is exactly 16 bytes. SELF decryption looks the
|
||||
matching file up in that folder by content id, with a lowercase extension.
|
||||
source_ref: "app/src/main/java/aenu/aps3e/MainActivity.java:702-706, app/src/main/java/aenu/aps3e/Emulator.java:820-844, app/src/main/cpp/rpcs3/rpcs3/Crypto/unself.cpp:1271-1286, app/src/main/cpp/rpcs3/rpcs3/Emu/system_utils.cpp:391"
|
||||
matching file up by content id, with a lowercase extension, in the exdata
|
||||
folder of each user under dev_hdd0/home, and rejects a content id longer
|
||||
than 0x30 characters or outside A-Z, a-z, 0-9, - and _.
|
||||
source_ref: "app/src/main/java/aenu/aps3e/MainActivity.java:649-653, app/src/main/java/aenu/aps3e/Emulator.java:820-845, app/src/main/cpp/rpcs3/rpcs3/Crypto/unself.cpp:1312-1343, app/src/main/cpp/rpcs3/rpcs3/Emu/system_utils.cpp:396-421"
|
||||
|
||||
- name: "<content_id>.edat"
|
||||
required: false
|
||||
@@ -158,7 +163,7 @@ files:
|
||||
Imported through the same entry. The NPD header is read and the file
|
||||
refused below 0x90 bytes or when the magic is not NPD, then written whole
|
||||
to dev_hdd0/home/00000001/exdata under the content id the header declares.
|
||||
source_ref: "app/src/main/java/aenu/aps3e/MainActivity.java:707-711, app/src/main/cpp/aps3e_emu.cpp:941-969, app/src/main/cpp/emulator_aps3e.cpp:1085, app/src/main/cpp/rpcs3/rpcs3/Emu/system_utils.cpp:413"
|
||||
source_ref: "app/src/main/java/aenu/aps3e/MainActivity.java:654-658, app/src/main/cpp/aps3e_emu.cpp:878-906, app/src/main/cpp/emulator_aps3e.cpp:1085, app/src/main/cpp/rpcs3/rpcs3/Emu/system_utils.cpp:440"
|
||||
|
||||
- name: "<font>.ttf"
|
||||
aliases: ["<font>.ttc", "<font>.otf"]
|
||||
@@ -172,7 +177,7 @@ files:
|
||||
classes at once, and only while Font File Selection is Custom. The setting
|
||||
reverts to From Firmware when the stored path no longer resolves, so the
|
||||
firmware faces cover the absence.
|
||||
source_ref: "app/src/main/java/aenu/aps3e/EmulatorSettings.java:1332-1352, app/src/main/cpp/aps3e_rp3_impl.cpp:227-239, app/src/main/cpp/rpcs3/rpcs3/Emu/system_config.h:401-402"
|
||||
source_ref: "app/src/main/java/aenu/aps3e/EmulatorSettings.java:1329-1349, app/src/main/cpp/aps3e_rp3_impl.cpp:215-227, app/src/main/cpp/rpcs3/rpcs3/Emu/system_config.h:402-403"
|
||||
|
||||
- name: "<driver>.so"
|
||||
aliases: ["<driver>.zip"]
|
||||
@@ -183,22 +188,23 @@ files:
|
||||
note: >-
|
||||
Imported as a shared object, or as a zip whose meta.json names the library
|
||||
to keep, and unpacked into the driver directory in internal storage. Read
|
||||
from the global config YAML before the emulator initialises and dlopened in
|
||||
place of the system loader. The system libvulkan.so is used when Use Custom
|
||||
Driver is off, the path is empty, or the file is gone.
|
||||
source_ref: "app/src/main/java/aenu/aps3e/EmulatorSettings.java:1235-1330, app/src/main/cpp/aps3e_emu.cpp:1015-1039, app/src/main/cpp/vkapi.cpp:21, app/src/main/cpp/rpcs3/rpcs3/Emu/system_config.h:209-211"
|
||||
from the global config YAML before the emulator initialises and opened
|
||||
through libadrenotools in place of the system loader. The system
|
||||
libvulkan.so is used when Use Custom Driver is off, the path is empty, or
|
||||
the file is gone.
|
||||
source_ref: "app/src/main/java/aenu/aps3e/EmulatorSettings.java:1232-1327, app/src/main/cpp/aps3e_emu.cpp:952-976, app/src/main/cpp/vkapi.cpp:18-33, app/src/main/cpp/rpcs3/rpcs3/Emu/system_config.h:209-211"
|
||||
|
||||
# Layout produced by extracting PS3UPDAT.PUP, and the paths this tree reads from it.
|
||||
dev_flash:
|
||||
path: "config/dev_flash/"
|
||||
source_ref: "app/src/main/cpp/rpcs3/rpcs3/Emu/vfs_config.h:15, app/src/main/cpp/aps3e_rp3_impl.cpp:151-153"
|
||||
source_ref: "app/src/main/cpp/rpcs3/rpcs3/Emu/vfs_config.h:15, app/src/main/cpp/aps3e_rp3_impl.cpp:139-141"
|
||||
note: >-
|
||||
Mounted at /dev_flash and rooted at the config directory. Written only by the
|
||||
firmware installer, which extracts the dev_flash_ packages of the PUP into it.
|
||||
|
||||
sys_external:
|
||||
path: "dev_flash/sys/external/"
|
||||
source_ref: "app/src/main/cpp/rpcs3/rpcs3/Emu/Cell/PPUModule.cpp:2608-2610, app/src/main/cpp/rpcs3/rpcs3/Emu/Cell/lv2/sys_prx.cpp:207"
|
||||
source_ref: "app/src/main/cpp/rpcs3/rpcs3/Emu/Cell/PPUModule.cpp:2703-2705, app/src/main/cpp/rpcs3/rpcs3/Emu/Cell/lv2/sys_prx.cpp:208, app/src/main/cpp/rpcs3/rpcs3/Emu/Cell/Modules/cellSysmodule.cpp:1031"
|
||||
note: >-
|
||||
Firmware SPRX modules. liblv2.sprx gates the boot and pulls the rest in;
|
||||
each module can be forced to LLE or HLE through Libraries Control, which
|
||||
@@ -213,7 +219,7 @@ dev_flash:
|
||||
|
||||
fonts:
|
||||
path: "dev_flash/data/font/"
|
||||
source_ref: "app/src/main/cpp/rpcs3/rpcs3/Emu/Cell/Modules/cellFont.cpp:226-241, app/src/main/cpp/rpcs3/rpcs3/Emu/Cell/Modules/cellFont.cpp:281, app/src/main/cpp/rpcs3/rpcs3/Emu/RSX/Overlays/overlay_fonts.cpp:117-118"
|
||||
source_ref: "app/src/main/cpp/rpcs3/rpcs3/Emu/Cell/Modules/cellFont.cpp:226-241, app/src/main/cpp/rpcs3/rpcs3/Emu/Cell/Modules/cellFont.cpp:281, app/src/main/cpp/rpcs3/rpcs3/Emu/RSX/Overlays/overlay_fonts.cpp:116-117"
|
||||
note: >-
|
||||
Faces cellFontOpenFontset maps by type, and the directory the overlay
|
||||
searches for its own list. A fontset the emulator does not implement falls
|
||||
@@ -244,21 +250,21 @@ dev_flash:
|
||||
|
||||
vsh:
|
||||
path: "dev_flash/vsh/"
|
||||
source_ref: "app/src/main/cpp/rpcs3/rpcs3/util/sysinfo.cpp:651-653, app/src/main/cpp/rpcs3/rpcs3/Emu/Cell/PPUThread.cpp:4433-4441, app/src/main/cpp/rpcs3/rpcs3/Emu/RSX/Overlays/overlay_user_list_dialog.cpp:207"
|
||||
source_ref: "app/src/main/cpp/rpcs3/rpcs3/util/sysinfo.cpp:651-653, app/src/main/cpp/rpcs3/rpcs3/Emu/Cell/PPUThread.cpp:4799-4807, app/src/main/cpp/rpcs3/rpcs3/Emu/RSX/Overlays/overlay_user_list_dialog.cpp:207, app/src/main/cpp/rpcs3/rpcs3/Emu/RSX/Overlays/FriendsList/overlay_friends_list_dialog.cpp:85"
|
||||
note: >-
|
||||
vsh/etc/version.txt supplies the installed firmware version, compared
|
||||
against the PS3_SYSTEM_VER a title declares. vsh/module/eseibrd.sprx is
|
||||
precompiled at boot and vsh/resource/explore/user carries the avatars the
|
||||
user list draws.
|
||||
user list and the friends list draw.
|
||||
|
||||
ps1emu:
|
||||
path: "dev_flash/ps1emu/"
|
||||
source_ref: "app/src/main/cpp/rpcs3/rpcs3/Emu/System.cpp:2187"
|
||||
source_ref: "app/src/main/cpp/rpcs3/rpcs3/Emu/System.cpp:2253"
|
||||
note: "ps1_newemu.self, the argv[0] of a PS1 Classics boot."
|
||||
|
||||
pspemu:
|
||||
path: "dev_flash/pspemu/"
|
||||
source_ref: "app/src/main/cpp/rpcs3/rpcs3/Emu/System.cpp:2215"
|
||||
source_ref: "app/src/main/cpp/rpcs3/rpcs3/Emu/System.cpp:2281"
|
||||
note: "psp_emulator.self, the argv[0] of a PSP minis boot."
|
||||
|
||||
dev_flash2:
|
||||
|
||||
+76
-57
@@ -3,8 +3,9 @@ type: standalone + libretro
|
||||
core_classification: community_fork
|
||||
source: "https://github.com/libretro/libretro-cap32"
|
||||
upstream: "https://github.com/ColinPitrat/caprice32"
|
||||
profiled_date: "2026-03-23"
|
||||
source_commit: "9171747fe9f36fb3ed9c61f65caaaef4858d49a5"
|
||||
profiled_date: "2026-10-06"
|
||||
source_commit: "c23855092f5f49981a5096f4d39033ff573ebca7"
|
||||
upstream_commit: "6c12c4c92360065cdc229ac9ada7551f941436b8"
|
||||
core_version: "v4.2.0"
|
||||
display_name: "Amstrad - CPC/GX4000 (Caprice32)"
|
||||
cores:
|
||||
@@ -15,107 +16,125 @@ systems:
|
||||
|
||||
notes: |
|
||||
Caprice32 is an Amstrad CPC emulator ported to libretro. Supports CPC 464,
|
||||
CPC 664, CPC 6128, and CPC+ (Plus) models.
|
||||
CPC 464 with DDI-1 disc interface, CPC 664, CPC 6128, and CPC+ (Plus) models.
|
||||
|
||||
All system ROMs are compiled directly into the core binary as C header
|
||||
arrays in cap32/rom/*.h. No external BIOS files are required.
|
||||
|
||||
Embedded ROMs (cap32/cap32.c:348-351):
|
||||
- cap32/rom/464.h: OS_BASIC10[32768] - CPC 464 OS + BASIC 1.0
|
||||
- cap32/rom/6128.h: OS_BASIC11[32768] - CPC 6128 OS + BASIC 1.1
|
||||
- cap32/rom/amsdos.h: AMSDOS[16384] - AMSDOS disk controller ROM
|
||||
- cap32/rom/6128p.h: OS_6128P - CPC+ system cartridge ROM
|
||||
- cap32/rom/cpm.h: cpmROM - CP/M boot snapshot (128KB)
|
||||
Embedded ROMs (cap32/cap32.c:345-349, cap32/slots.c:33):
|
||||
- cap32/rom/464.h: OS464_BASIC10[32768] - CPC 464 OS + BASIC 1.0
|
||||
- cap32/rom/664.h: OS664_BASIC664[32768] - CPC 664 OS + BASIC 1.1
|
||||
- cap32/rom/6128.h: OS_BASIC11[32768] - CPC 6128 OS + BASIC 1.1
|
||||
- cap32/rom/amsdos.h: AMSDOS[16384] - AMSDOS disc ROM
|
||||
- cap32/rom/6128p.h: OS_6128P[65580] - CPC+ system cartridge
|
||||
- cap32/rom/cpm.h: CPM, CPM_SYS - CP/M boot memory dumps
|
||||
|
||||
ROM selection in emulator_select_ROM() (cap32/cap32.c:1084-1112):
|
||||
- CPC 464: OS_BASIC10 (32KB)
|
||||
- CPC 664: OS_BASIC10 (32KB) + AMSDOS in slot 7
|
||||
- CPC 6128: OS_BASIC11 (32KB) + AMSDOS in slot 7
|
||||
- CPC+: OS_6128P loaded as system cartridge via cpr_load()
|
||||
ROM selection in emulator_select_ROM() (cap32/cap32.c:1082-1114):
|
||||
- CPC 464: OS464_BASIC10
|
||||
- CPC 464DDI: OS464_BASIC10 + AMSDOS in slot 7
|
||||
- CPC 664: OS664_BASIC664 + AMSDOS in slot 7
|
||||
- CPC 6128: OS_BASIC11 + AMSDOS in slot 7
|
||||
- CPC+: OS_6128P loaded as system cartridge via cpr_load()
|
||||
|
||||
The standalone (non-libretro) version references external filenames
|
||||
cpc464.rom, cpc664.rom, cpc6128.rom, amsdos.rom in cap32/cap32.c:355-358
|
||||
via config file loading (cap32/cap32.c:1854-1866), but the libretro port
|
||||
does not use this path. The libretro init (libretro/libretro-core.c:1612-1618) has
|
||||
a "TODO: future use to load custom bios" comment that is not
|
||||
implemented.
|
||||
Core option "cap32_model" accepts "464DDI" (libretro/libretro-core.c:501,
|
||||
libretro/libretro-core.c:1035). A 464 loading disc content is switched to
|
||||
464DDI (libretro/libretro-core.c:1364-1368).
|
||||
|
||||
The array chROMFile names "cpc464.rom", "cpc664.rom", "cpc6128.rom"
|
||||
(cap32/cap32.c:353-357) and loadConfiguration() parses the rom_path,
|
||||
slotNN and rom_mf2 keys of ./cap32.cfg (cap32/cap32.c:1846-1863), but no
|
||||
code opens these files. The system directory is resolved in
|
||||
libretro/libretro-core.c:1617-1636 under a "TODO: future use to load
|
||||
custom bios" comment and no ROM is read from it.
|
||||
|
||||
Keyboard layout patches are applied to the ROM in memory for French
|
||||
and Spanish layouts via rom_mods.h (cap32/cap32.c:1114-1138).
|
||||
and Spanish layouts via rom_mods.h (cap32/cap32.c:1116-1141).
|
||||
|
||||
CP/M support loads a pre-built snapshot (cpm.h) into memory via
|
||||
snapshot_load_mem() in cap32/slots.c:93.
|
||||
CP/M support assembles a 128KB snapshot from the CPM and CPM_SYS arrays
|
||||
and loads it via snapshot_load_mem() in cap32/slots.c:71-90.
|
||||
|
||||
files:
|
||||
# Standalone loads from rom_path/ — libretro embeds all as C arrays
|
||||
# ref: ColinPitrat/caprice32/src/cap32.cpp:1006-1007,1159-1171
|
||||
# Standalone loads from rom_path/; libretro embeds all as C arrays
|
||||
- name: cpc464.rom
|
||||
system: amstrad-cpc
|
||||
required: true
|
||||
required: false
|
||||
mode: standalone
|
||||
size: 32768
|
||||
note: "CPC 464 OS + BASIC 1.0"
|
||||
source_ref: "src/cap32.cpp:214, rom/cpc464.rom"
|
||||
md5: a993f85b88ac4350cf4d41554e87fe4f
|
||||
sha1: 56d39c463da60968d93e58b4ba0e675829412a20
|
||||
note: "CPC 464 OS + BASIC 1.0, read when model=0"
|
||||
source_ref: "src/cap32.cpp:212-217, src/cap32.cpp:1013-1027, rom/cpc464.rom"
|
||||
- name: cpc664.rom
|
||||
system: amstrad-cpc
|
||||
required: false
|
||||
mode: standalone
|
||||
size: 32768
|
||||
note: "CPC 664 OS + BASIC 1.0"
|
||||
source_ref: "caprice32/rom/cpc664.rom"
|
||||
md5: 5a384a2310f472c7857888371c00ed66
|
||||
sha1: 073a7665527b5bd8a148747a3947dbd3328682c8
|
||||
note: "CPC 664 OS + BASIC 1.1, read when model=1"
|
||||
source_ref: "src/cap32.cpp:212-217, src/cap32.cpp:1013-1027, rom/cpc664.rom"
|
||||
- name: cpc6128.rom
|
||||
system: amstrad-cpc
|
||||
required: false
|
||||
required: true
|
||||
mode: standalone
|
||||
size: 32768
|
||||
note: "CPC 6128 OS + BASIC 1.1"
|
||||
source_ref: "caprice32/rom/cpc6128.rom"
|
||||
md5: b96280dc6c95a48857b4b8eb931533ae
|
||||
sha1: 5977adbad3f7c1e0e082cd02fe76a700d9860c30
|
||||
note: "CPC 6128 OS + BASIC 1.1, default model=2; missing file returns ERR_CPC_ROM_MISSING"
|
||||
source_ref: "src/cap32.cpp:212-217, src/cap32.cpp:1013-1027, src/cap32.cpp:1799, rom/cpc6128.rom"
|
||||
- name: amsdos.rom
|
||||
system: amstrad-cpc
|
||||
required: false
|
||||
mode: standalone
|
||||
size: 16384
|
||||
note: "AMSDOS disc ROM (sideways slot 7)"
|
||||
source_ref: "src/cap32.cpp:1262"
|
||||
md5: 25629dfe870d097469c217b95fdc1c95
|
||||
sha1: 39102c8e9cb55fcc0b9b62098780ed4a3cb6a4bb
|
||||
note: "AMSDOS disc ROM, shipped cap32.cfg sets slot07=DEFAULT, skipped on the 464; missing file clears the slot"
|
||||
source_ref: "src/cap32.cpp:1170-1178, src/cap32.cpp:1245-1249, cap32.cfg:215, rom/amsdos.rom"
|
||||
- name: system.cpr
|
||||
system: amstrad-cpc
|
||||
required: false
|
||||
mode: standalone
|
||||
size: 131156
|
||||
note: "CPC+ system cartridge"
|
||||
source_ref: "caprice32/rom/system.cpr"
|
||||
md5: 39b565e648172980aa552293e667070f
|
||||
sha1: 405e341437fab8db477606238af0c20eca2e39ee
|
||||
note: "CPC+ system cartridge, default cartridge path"
|
||||
source_ref: "src/cap32.cpp:1904, src/cap32.cpp:1150, rom/system.cpr"
|
||||
- name: MF2.rom
|
||||
system: amstrad-cpc
|
||||
required: false
|
||||
mode: standalone
|
||||
size: 8192
|
||||
note: "Multiface 2 ROM"
|
||||
source_ref: "caprice32/rom/MF2.rom, src/cap32.cpp:1340-1359"
|
||||
md5: 4c9f014e91d407cc6509fad0095ab909
|
||||
sha1: 1431ec628d38f000715545dd2186b684c5fe5a6f
|
||||
note: "Multiface 2 ROM, read when mf2=1; checks the \"MULTIFACE 2\" signature at 0x0d32"
|
||||
source_ref: "src/cap32.cpp:1252-1278, rom/MF2.rom"
|
||||
|
||||
platform_details:
|
||||
bios_mapping:
|
||||
source_ref: "cap32/cap32.c:1084-1112, cap32/cap32.c:348-351"
|
||||
source_ref: "cap32/cap32.c:1082-1114, cap32/cap32.c:345-349"
|
||||
note: |
|
||||
All ROMs are embedded. emulator_select_ROM() copies the appropriate
|
||||
ROM array into the pbROM buffer based on CPC.model. No filesystem
|
||||
lookup occurs in the libretro port. The retro_system_bios_directory
|
||||
variable is set up in libretro/libretro-core.c:1616-1627 but only used for
|
||||
a future custom BIOS feature that is not yet implemented.
|
||||
All ROMs are embedded. emulator_select_ROM() copies the ROM array for
|
||||
CPC.model into pbROM. No filesystem lookup occurs in the libretro
|
||||
port; retro_system_bios_directory is set in
|
||||
libretro/libretro-core.c:1617-1636 and no ROM is read from it.
|
||||
|
||||
embedded_roms:
|
||||
source_ref: "cap32/rom/464.h, cap32/rom/6128.h, cap32/rom/amsdos.h, cap32/rom/6128p.h, cap32/rom/cpm.h"
|
||||
source_ref: "cap32/rom/464.h, cap32/rom/664.h, cap32/rom/6128.h, cap32/rom/amsdos.h, cap32/rom/6128p.h, cap32/rom/cpm.h"
|
||||
note: |
|
||||
5 ROM sets embedded as C arrays:
|
||||
OS_BASIC10 (32KB) - CPC 464/664 OS + BASIC 1.0
|
||||
OS_BASIC11 (32KB) - CPC 6128 OS + BASIC 1.1
|
||||
AMSDOS (16KB) - Disk controller ROM, mapped to slot 7
|
||||
OS_6128P - CPC+ system cartridge (White Label cartridge)
|
||||
cpmROM (128KB+) - CP/M boot snapshot including SNA header
|
||||
6 headers embedded as C arrays:
|
||||
OS464_BASIC10 (32KB) - CPC 464 OS + BASIC 1.0, sha1 56d39c463da60968d93e58b4ba0e675829412a20
|
||||
OS664_BASIC664 (32KB) - CPC 664 OS + BASIC 1.1, sha1 073a7665527b5bd8a148747a3947dbd3328682c8
|
||||
OS_BASIC11 (32KB) - CPC 6128 OS + BASIC 1.1, sha1 5977adbad3f7c1e0e082cd02fe76a700d9860c30
|
||||
AMSDOS (16KB) - disc ROM mapped to slot 7, sha1 39102c8e9cb55fcc0b9b62098780ed4a3cb6a4bb
|
||||
OS_6128P (65580 B) - CPC+ system cartridge
|
||||
CPM, CPM_SYS - CP/M memory dumps assembled into a 128KB snapshot
|
||||
|
||||
model_rom_map:
|
||||
source_ref: "cap32/cap32.c:1084-1112"
|
||||
source_ref: "cap32/cap32.c:1082-1114"
|
||||
note: |
|
||||
CPC 464: OS_BASIC10 only (no disk support)
|
||||
CPC 664: OS_BASIC10 + AMSDOS in ROM slot 7
|
||||
CPC 6128: OS_BASIC11 + AMSDOS in ROM slot 7
|
||||
CPC+: 6128P system cartridge (or user-provided .cpr file)
|
||||
CPC 464: OS464_BASIC10 only
|
||||
CPC 464DDI: OS464_BASIC10 + AMSDOS in ROM slot 7
|
||||
CPC 664: OS664_BASIC664 + AMSDOS in ROM slot 7
|
||||
CPC 6128: OS_BASIC11 + AMSDOS in ROM slot 7
|
||||
CPC+: 6128P system cartridge (or user-provided .cpr file)
|
||||
@@ -24,7 +24,7 @@ files:
|
||||
hle_fallback: true
|
||||
size: 16384
|
||||
validation: [size]
|
||||
source_ref: "desmume/src/NDSSystem.cpp:2178-2182, desmume/src/frontend/libretro/libretro.cpp:763"
|
||||
source_ref: "desmume/src/NDSSystem.cpp:2156-2166, desmume/src/frontend/libretro/libretro.cpp:763"
|
||||
note: "Native file needed for firmware boot and SWI-from-BIOS accuracy"
|
||||
|
||||
- name: "bios9.bin"
|
||||
@@ -34,7 +34,7 @@ files:
|
||||
hle_fallback: true
|
||||
size: 4096
|
||||
validation: [size]
|
||||
source_ref: "desmume/src/NDSSystem.cpp:2236-2239, desmume/src/frontend/libretro/libretro.cpp:764"
|
||||
source_ref: "desmume/src/NDSSystem.cpp:2214-2224, desmume/src/frontend/libretro/libretro.cpp:764"
|
||||
note: "Native file needed for firmware boot and logo comparison (Pal Park)"
|
||||
|
||||
- name: "firmware.bin"
|
||||
|
||||
+3
-3
@@ -23,7 +23,7 @@ notes: |
|
||||
builtin - always use the built-in open-source BIOS
|
||||
official - try gba_bios.bin, warn and fall back to built-in if missing
|
||||
|
||||
BIOS loading path (libretro/libretro.c:1200-1236):
|
||||
BIOS loading path (libretro/libretro.c:1239-1276):
|
||||
1. Resolve system directory (RETRO_ENVIRONMENT_GET_SYSTEM_DIRECTORY)
|
||||
2. Append "/gba_bios.bin"
|
||||
3. load_bios() reads 0x4000 (16 KB) into bios_rom[]
|
||||
@@ -44,13 +44,13 @@ files:
|
||||
size: 16384
|
||||
description: "Official GBA BIOS"
|
||||
note: "Built-in open-source BIOS used as fallback. Real BIOS needed for full SWI accuracy and boot animation."
|
||||
source_ref: "libretro/libretro.c:1214, gba_memory.c:2568-2580"
|
||||
source_ref: "libretro/libretro.c:1254, gba_memory.c:3270-3281"
|
||||
|
||||
platform_details:
|
||||
gba:
|
||||
bios_size: 16384
|
||||
hle_bios: true
|
||||
source_ref: "libretro/libretro.c:1200-1236, bios/source/core.s"
|
||||
source_ref: "libretro/libretro.c:1239-1276, bios/source/core.s"
|
||||
note: |
|
||||
The built-in BIOS (open_gba_bios_rom, 16 KB) is an open-source
|
||||
reimplementation compiled from bios/source/. It implements most
|
||||
|
||||
@@ -64,6 +64,7 @@ files:
|
||||
# -------------------------------------------------------
|
||||
- name: "us_scd2_9306.bin"
|
||||
system: sega-segacd
|
||||
variant_group: "mega-cd-bios"
|
||||
region: [north-america]
|
||||
priority: 1
|
||||
required: true
|
||||
@@ -73,6 +74,7 @@ files:
|
||||
|
||||
- name: "SegaCDBIOS9303.bin"
|
||||
system: sega-segacd
|
||||
variant_group: "mega-cd-bios"
|
||||
region: [north-america]
|
||||
priority: 2
|
||||
required: false
|
||||
@@ -82,6 +84,7 @@ files:
|
||||
|
||||
- name: "us_scd1_9210.bin"
|
||||
system: sega-segacd
|
||||
variant_group: "mega-cd-bios"
|
||||
region: [north-america]
|
||||
priority: 3
|
||||
required: false
|
||||
@@ -91,6 +94,7 @@ files:
|
||||
|
||||
- name: "bios_CD_U.bin"
|
||||
system: sega-segacd
|
||||
variant_group: "mega-cd-bios"
|
||||
region: [north-america]
|
||||
priority: 4
|
||||
required: false
|
||||
@@ -103,6 +107,7 @@ files:
|
||||
# -------------------------------------------------------
|
||||
- name: "eu_mcd2_9306.bin"
|
||||
system: sega-megacd
|
||||
variant_group: "mega-cd-bios"
|
||||
region: [europe]
|
||||
priority: 1
|
||||
required: true
|
||||
@@ -112,6 +117,7 @@ files:
|
||||
|
||||
- name: "eu_mcd2_9303.bin"
|
||||
system: sega-megacd
|
||||
variant_group: "mega-cd-bios"
|
||||
region: [europe]
|
||||
priority: 2
|
||||
required: false
|
||||
@@ -121,6 +127,7 @@ files:
|
||||
|
||||
- name: "eu_mcd1_9210.bin"
|
||||
system: sega-megacd
|
||||
variant_group: "mega-cd-bios"
|
||||
region: [europe]
|
||||
priority: 3
|
||||
required: false
|
||||
@@ -130,6 +137,7 @@ files:
|
||||
|
||||
- name: "bios_CD_E.bin"
|
||||
system: sega-megacd
|
||||
variant_group: "mega-cd-bios"
|
||||
region: [europe]
|
||||
priority: 4
|
||||
required: false
|
||||
@@ -142,6 +150,7 @@ files:
|
||||
# -------------------------------------------------------
|
||||
- name: "jp_mcd2_921222.bin"
|
||||
system: sega-megacd
|
||||
variant_group: "mega-cd-bios"
|
||||
region: [japan]
|
||||
priority: 1
|
||||
required: true
|
||||
@@ -151,6 +160,7 @@ files:
|
||||
|
||||
- name: "jp_mcd1_9112.bin"
|
||||
system: sega-megacd
|
||||
variant_group: "mega-cd-bios"
|
||||
region: [japan]
|
||||
priority: 2
|
||||
required: false
|
||||
@@ -160,6 +170,7 @@ files:
|
||||
|
||||
- name: "jp_mcd1_9111.bin"
|
||||
system: sega-megacd
|
||||
variant_group: "mega-cd-bios"
|
||||
region: [japan]
|
||||
priority: 3
|
||||
required: false
|
||||
@@ -169,6 +180,7 @@ files:
|
||||
|
||||
- name: "bios_CD_J.bin"
|
||||
system: sega-megacd
|
||||
variant_group: "mega-cd-bios"
|
||||
region: [japan]
|
||||
priority: 4
|
||||
required: false
|
||||
|
||||
@@ -108,14 +108,14 @@ files:
|
||||
source_ref: "src/LIBRETRO/libretro.c:559"
|
||||
aliases: ["N88N.ROM", "N80.ROM", "n80.rom"]
|
||||
|
||||
- name: disk.rom
|
||||
path: "quasi88/disk.rom"
|
||||
- name: n88sub.rom
|
||||
path: "quasi88/n88sub.rom"
|
||||
size: 2048
|
||||
required: false
|
||||
hle_fallback: true
|
||||
note: "Disk subsystem ROM (2 KB, 2D type). Falls back to pseudo-BIOS if missing."
|
||||
source_ref: "src/LIBRETRO/libretro.c:552-553"
|
||||
aliases: ["DISK.ROM", "N88SUB.ROM", "n88sub.rom"]
|
||||
source_ref: "src/LIBRETRO/libretro.c:114, src/LIBRETRO/libretro.c:552-553"
|
||||
aliases: ["N88SUB.ROM", "DISK.ROM", "disk.rom"]
|
||||
|
||||
- name: n88knj1.rom
|
||||
path: "quasi88/n88knj1.rom"
|
||||
|
||||
+451
-441
File diff suppressed because it is too large.
Load diff
+229
-229
@@ -5,7 +5,7 @@
|
||||
"platform": "bizhawk",
|
||||
"display_name": "BizHawk",
|
||||
"version": "1.0",
|
||||
"generated": "2026-10-05T00:24:12Z",
|
||||
"generated": "2026-10-05T20:14:26Z",
|
||||
"base_destination": "Firmware",
|
||||
"detect": [
|
||||
{
|
||||
@@ -21,9 +21,9 @@
|
||||
],
|
||||
"standalone_copies": [],
|
||||
"total_files": 816,
|
||||
"total_size": 2540396315,
|
||||
"total_size": 2570871683,
|
||||
"pack_files": 818,
|
||||
"pack_size": 2540396315,
|
||||
"pack_size": 2570871683,
|
||||
"total_omitted": 2,
|
||||
"omitted_files": [
|
||||
{
|
||||
@@ -1526,10 +1526,10 @@
|
||||
},
|
||||
{
|
||||
"dest": "fbneo/hiscore.dat",
|
||||
"sha1": "7381472bf046126257e51a0124e4553282f020e5",
|
||||
"sha256": "9d44fc928c0df41debad1b86739bd6ca96a5891b3b499f6fb3d7e9ea95ca8de5",
|
||||
"size": 322458,
|
||||
"repo_path": "bios/Arcade/Arcade/hiscore.dat",
|
||||
"sha1": "ae3329abbcb7b445d979321a1f67733aa3f023d0",
|
||||
"sha256": "d9c51a13ec2c6db6c9ba0f955c8a8fd8fe54fc2ec05cd82226f4ddf53d500182",
|
||||
"size": 314122,
|
||||
"repo_path": "bios/Arcade/FBNeo/hiscore.dat",
|
||||
"cores": [
|
||||
"fbneo"
|
||||
]
|
||||
@@ -1782,10 +1782,10 @@
|
||||
},
|
||||
{
|
||||
"dest": "neogeo.zip",
|
||||
"sha1": "deb62b0074b8cae4f162c257662136733cfc76ad",
|
||||
"sha256": "bf05cc848e403f8ae457bfe26faa8135cf4725cfff956fa030f5c0a00a26adcc",
|
||||
"size": 1859335,
|
||||
"repo_path": "bios/Arcade/Arcade/neogeo.zip",
|
||||
"sha1": "447e23757e5be7ae688885a5ce8ea3da8c8bedc0",
|
||||
"sha256": "9a8cf2b88488969d7000028b30474175067f753a649a1fe0c008efd8450d60a0",
|
||||
"size": 1952551,
|
||||
"repo_path": "bios/Arcade/MAME/neogeo.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
@@ -1802,20 +1802,20 @@
|
||||
},
|
||||
{
|
||||
"dest": "naomi.zip",
|
||||
"sha1": "788aee0f30ee80ea54dcd705afe93944accafc31",
|
||||
"sha256": "62a011c3a4dc5686ebced343987199bd7ccad1578ca8e2f2350b41204d87de65",
|
||||
"size": 9651827,
|
||||
"repo_path": "bios/Arcade/Arcade/naomi.zip",
|
||||
"sha1": "a39d7d17e52c26435ed1745503c7d6a68b36be1d",
|
||||
"sha256": "9b85d2183bc3739efa2ebf1440c73abb252c6ca8944efcff3a7204645f7e03d4",
|
||||
"size": 10419595,
|
||||
"repo_path": "bios/Arcade/MAME/naomi.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
},
|
||||
{
|
||||
"dest": "naomi2.zip",
|
||||
"sha1": "da79eca49c59f7661737643b5e11e63a73b51fcc",
|
||||
"sha256": "5efc11a37e0b8fe97e0007bb707d8a4f047dd0b8b7ba59293e81cca0443f7993",
|
||||
"size": 7038226,
|
||||
"repo_path": "bios/Arcade/Arcade/naomi2.zip",
|
||||
"sha1": "b42c05722fa25416a382f87113132131153363f7",
|
||||
"sha256": "f227eade732c9f77fda75f7084703b6d12bfcdb83a726a0b1509abe04f13fe5e",
|
||||
"size": 7573531,
|
||||
"repo_path": "bios/Arcade/MAME/naomi2.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
@@ -1852,20 +1852,20 @@
|
||||
},
|
||||
{
|
||||
"dest": "f355dlx.zip",
|
||||
"sha1": "48d1712d1b1cdfeeeb43c6287c17b0b6309cfaab",
|
||||
"sha256": "58e88cedb31918e2b9deeaddb80df45b8484cc1c4f10792520360a2585ad5014",
|
||||
"size": 2328436,
|
||||
"repo_path": "bios/Arcade/Arcade/f355dlx.zip",
|
||||
"sha1": "7740229e15dd80b20a8d263a67cd37c310602ee4",
|
||||
"sha256": "92d8634d8c61a1b50aa0b517638d8829bb70e7fe051423695722e77869d6780c",
|
||||
"size": 2331068,
|
||||
"repo_path": "bios/Arcade/MAME/f355dlx.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
},
|
||||
{
|
||||
"dest": "f355bios.zip",
|
||||
"sha1": "b6ff66dcb5547bd91760d239ddf428a655631c53",
|
||||
"sha256": "8335208b035253d5c671ecaaa113369409ffd70e86d1c87ab1e21bdf352fff6c",
|
||||
"size": 1394278,
|
||||
"repo_path": "bios/Arcade/Arcade/f355bios.zip",
|
||||
"sha1": "37487f9f73734e8dd621004a5ef98c5a2301bd1d",
|
||||
"sha256": "47ebe6dad9d8398c7eacb69c652d8f5c449c6f7ce420579c3a287c1b73e0250b",
|
||||
"size": 1396910,
|
||||
"repo_path": "bios/Arcade/MAME/f355bios.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
@@ -1882,20 +1882,20 @@
|
||||
},
|
||||
{
|
||||
"dest": "lindbios.zip",
|
||||
"sha1": "65a2f2cee74c316d5f40b68deda66787609df353",
|
||||
"sha256": "9d057f11a2c4cfa7c637016fbca20c125c86a137ab4ca1957079f5cf0e73000c",
|
||||
"size": 1462941,
|
||||
"repo_path": "bios/Arcade/Arcade/lindbios.zip",
|
||||
"sha1": "cd2601242084856146bae2bea3084930b423e744",
|
||||
"sha256": "0fadf3027e8b690de567140c3ed72885da25c18471345e9adc18d3aa879a6859",
|
||||
"size": 1462963,
|
||||
"repo_path": "bios/Arcade/MAME/lindbios.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
},
|
||||
{
|
||||
"dest": "segasp.zip",
|
||||
"sha1": "de78a0b8df78d5ffdb1a274f80d877a96ef204aa",
|
||||
"sha256": "3bca348667d6ff08fd5efe38eec35100ccbe0de3cd1e0b5dd72ae5c6cc201ee9",
|
||||
"size": 1281424,
|
||||
"repo_path": "bios/Arcade/Arcade/segasp.zip",
|
||||
"sha1": "b4c7e46fb510096acf8fb19fb99c8c5a8ab1d9e1",
|
||||
"sha256": "bda96939992d42410034caf3f5296d8fc63eced1693b5b522536c6ac717ded73",
|
||||
"size": 4266446,
|
||||
"repo_path": "bios/Arcade/MAME/segasp.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
@@ -1912,10 +1912,10 @@
|
||||
},
|
||||
{
|
||||
"dest": "chihiro.zip",
|
||||
"sha1": "4fe94baf28d658dd2651de4055a0ac33aa42aa0e",
|
||||
"sha256": "760e30588b1d0fc7a3ede0097a0a5ca423c26b9e6a11c40d9e7f285489b57e4f",
|
||||
"size": 2104141,
|
||||
"repo_path": "bios/Arcade/Arcade/chihiro.zip",
|
||||
"sha1": "459832de5d758b76ac85a6c07b693496cd5ad7f2",
|
||||
"sha256": "48a268250decb2ff9e5388def44f66f2a260da002162677cc96d55892fafa6ad",
|
||||
"size": 2194504,
|
||||
"repo_path": "bios/Arcade/MAME/chihiro.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
@@ -1932,50 +1932,50 @@
|
||||
},
|
||||
{
|
||||
"dest": "shtzone.zip",
|
||||
"sha1": "2adbde3ef74c431fbd700cffd4cd1d41251a8833",
|
||||
"sha256": "123e8fa1550b12d7c1d07dfe5936d7bed3bb345abd3d1d5e02ad851df8167cd3",
|
||||
"size": 3332,
|
||||
"repo_path": "bios/Arcade/Arcade/shtzone.zip",
|
||||
"sha1": "d08a271feaab04ac904e6357c23bca95c1cf57f1",
|
||||
"sha256": "a62534c0fafce53482946d7d1b72ca844979c4e77a892264e3038a5ad87ba658",
|
||||
"size": 3354,
|
||||
"repo_path": "bios/Arcade/MAME/shtzone.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
},
|
||||
{
|
||||
"dest": "sammymdl.zip",
|
||||
"sha1": "2c178083ac2906cecb7e331d7111f7766a353052",
|
||||
"sha256": "b624c087452f580d19629105c55fd322ca2474f3153c8e5d366a8cedc0eecf66",
|
||||
"size": 24090,
|
||||
"repo_path": "bios/Arcade/Arcade/sammymdl.zip",
|
||||
"sha1": "a2d1cfe34d5e62b7c8d6490383fb079ffe7f4a4a",
|
||||
"sha256": "9767a4757761956c220df202e6cce2821aeb57b1daef544ee233ae7fd5fa5bad",
|
||||
"size": 24112,
|
||||
"repo_path": "bios/Arcade/MAME/sammymdl.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
},
|
||||
{
|
||||
"dest": "megaplay.zip",
|
||||
"sha1": "c9ee16e26e03496195a7bff151efbdd89da01204",
|
||||
"sha256": "5ba0a73114eaa86f6f5e233f2d4e2f6a38c4c383b99abc17b75f23a0940b6ba5",
|
||||
"sha1": "9012d69ce8d2efe231012dc033d651065b5f4669",
|
||||
"sha256": "fa8e8dd81f091f44f52ebda692deb98057cec04c338fe1f1f5bfee9d7b75a659",
|
||||
"size": 49978,
|
||||
"repo_path": "bios/Arcade/Arcade/megaplay.zip",
|
||||
"repo_path": "bios/Arcade/MAME/megaplay.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
},
|
||||
{
|
||||
"dest": "megatech.zip",
|
||||
"sha1": "be1bcca32794d0eb36c86ae32a2a43c92834cdc6",
|
||||
"sha256": "74b1591205a6c6c8f6ab62da6868dbdec5b0c886d80aa3e020220bc684a3401b",
|
||||
"size": 24831,
|
||||
"repo_path": "bios/Arcade/Arcade/megatech.zip",
|
||||
"sha1": "6f93b0340588b6b03ff2cb4c3954c34ecf06aa9b",
|
||||
"sha256": "aeb7024d40a6a9ea85b13d086597ef6ee8ca43b00788347e5701852db8acd211",
|
||||
"size": 33137,
|
||||
"repo_path": "bios/Arcade/MAME/megatech.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
},
|
||||
{
|
||||
"dest": "isgsm.zip",
|
||||
"sha1": "f590ccf688b4c05fa1da5c5dd92c224545170c3b",
|
||||
"sha256": "c481bfde0ae68e5fbe70a406af1698fcba5b76d7e23c860c2715cacf019efad4",
|
||||
"size": 10207,
|
||||
"repo_path": "bios/Arcade/Arcade/isgsm.zip",
|
||||
"sha1": "6a19d46d3915f2bff823ac34327bb0976fb2fed7",
|
||||
"sha256": "2a7a1a7ee15b322356bcdfd46dd3592bcf6a9bf6cb04f4cd2cf27ea54dbb6665",
|
||||
"size": 10185,
|
||||
"repo_path": "bios/Arcade/MAME/isgsm.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
@@ -1992,50 +1992,50 @@
|
||||
},
|
||||
{
|
||||
"dest": "decocass.zip",
|
||||
"sha1": "1b33cf0e730a6cde96479b0face108b6931d433e",
|
||||
"sha256": "bd86d293abab2e816d307626cdd793e2da2ed69c4124bf92f7e7eca69f9a5096",
|
||||
"size": 22379,
|
||||
"repo_path": "bios/Arcade/Arcade/decocass.zip",
|
||||
"sha1": "cf6c5c284110bdac0496704884fcd4f29d8480d9",
|
||||
"sha256": "947956d4983e8500b22cd6c40a1cc5ca13328c9849352ea966ddc882e125ac57",
|
||||
"size": 13203,
|
||||
"repo_path": "bios/Arcade/MAME/decocass.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
},
|
||||
{
|
||||
"dest": "playch10.zip",
|
||||
"sha1": "125e74975da3f7ac643232210ab37c33e4ece9ca",
|
||||
"sha256": "4734e5323c91f01c6027c9c8deeb4ad6a0cc82395436f4df7390c5b02897f47a",
|
||||
"size": 23388,
|
||||
"repo_path": "bios/Arcade/Arcade/playch10.zip",
|
||||
"sha1": "7df6ca1485eb0a27e54713a73152b5b5919db0a0",
|
||||
"sha256": "3dda03e8ccac4bc24533b52555f625a6001fa919fbfed003739aa8b640f26042",
|
||||
"size": 48317,
|
||||
"repo_path": "bios/Arcade/MAME/playch10.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
},
|
||||
{
|
||||
"dest": "nss.zip",
|
||||
"sha1": "68b780f40623e51a421faf374ae5a84a53a81279",
|
||||
"sha256": "24d341ee618c8499178ee9b57bebb68f5418645cc2b4683af605a7db628b196e",
|
||||
"size": 50536,
|
||||
"repo_path": "bios/Arcade/Arcade/nss.zip",
|
||||
"sha1": "3cc9e4df2511ff1c963ee2dd5eca44bb2377c986",
|
||||
"sha256": "7fb88b9885b21f3406f9dc6cbb3cda852a039e168bc852d6da9ce5bde439516b",
|
||||
"size": 50555,
|
||||
"repo_path": "bios/Arcade/MAME/nss.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
},
|
||||
{
|
||||
"dest": "sfcbox.zip",
|
||||
"sha1": "14311945b06a5d013d1df5ce961ab47fbd46fb2a",
|
||||
"sha256": "75e95fb7431568fe4e71c0d71bbf41ebce3091a2d25ecc8da53f7eb115c4eba8",
|
||||
"size": 194344,
|
||||
"repo_path": "bios/Arcade/Arcade/sfcbox.zip",
|
||||
"sha1": "21194d81d1a27dbf688ea43cf79d312e4fe2d0ee",
|
||||
"sha256": "bb09a26b65fcc2d3217f9b0c8b7552de4df44f2f792d97ca7845114e464c8f50",
|
||||
"size": 242698,
|
||||
"repo_path": "bios/Arcade/MAME/sfcbox.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
},
|
||||
{
|
||||
"dest": "aleck64.zip",
|
||||
"sha1": "beaf97c4a0e0792b8db65648f9dabb6a54ae0549",
|
||||
"sha256": "53e9cb3814cfa086a4cb152d250a740d2a438aaadbeec248a86a1698e69dc24a",
|
||||
"size": 1804,
|
||||
"repo_path": "bios/Arcade/Arcade/aleck64.zip",
|
||||
"sha1": "6f2f76832d6dc2c18997421ba26d22d623715c77",
|
||||
"sha256": "9785ce8820981de663ad9114c11ed0b70f548c80c4eb699b56745ddc3d106673",
|
||||
"size": 1826,
|
||||
"repo_path": "bios/Arcade/MAME/aleck64.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
@@ -2062,20 +2062,20 @@
|
||||
},
|
||||
{
|
||||
"dest": "sys246.zip",
|
||||
"sha1": "fbba87e9d6852862252d31dd919f943c8edd7eb7",
|
||||
"sha256": "f3bb5f2101534172ec49f6e7b6eb66bb2a2116c953d64517fc098a51f042002a",
|
||||
"size": 580122,
|
||||
"repo_path": "bios/Arcade/Arcade/sys246.zip",
|
||||
"sha1": "b7452e0120830f0770c097d6a4ef94d61659e3e9",
|
||||
"sha256": "923f6dffdd94b496d47c4b065884eef820e66c7d9ad7ebc759bffcef1e571c1e",
|
||||
"size": 580144,
|
||||
"repo_path": "bios/Arcade/MAME/sys246.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
},
|
||||
{
|
||||
"dest": "sys256.zip",
|
||||
"sha1": "721621a353051fea6de484c26b534cdec4de616e",
|
||||
"sha256": "5fc6a17d793b5b647c7590f650ee3f21c5210cca7e05e51734a3c3d20182698d",
|
||||
"size": 582110,
|
||||
"repo_path": "bios/Arcade/Arcade/sys256.zip",
|
||||
"sha1": "481e24901892848d005e5d9466d57ab3432b9b29",
|
||||
"sha256": "dca31e0839b94f0bf9d83ced74e4eb5612a5aed12df82d036f5a8c2acdaf3a5b",
|
||||
"size": 582132,
|
||||
"repo_path": "bios/Arcade/MAME/sys256.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
@@ -2092,40 +2092,40 @@
|
||||
},
|
||||
{
|
||||
"dest": "konamigx.zip",
|
||||
"sha1": "8cf0aa7f9dca4d77485e605fb0e2173a734633bf",
|
||||
"sha256": "a59e2233cb32b9292e6287736f73251f3e8d590810f18d38b8f8b28492d39505",
|
||||
"sha1": "a97c0579198d88d4db3f7f1b7707ddc6a86ab084",
|
||||
"sha256": "47be9a397b67ed8471223119caf59ea1284887b67ff573405671a7190f211d71",
|
||||
"size": 8712,
|
||||
"repo_path": "bios/Arcade/Arcade/konamigx.zip",
|
||||
"repo_path": "bios/Arcade/MAME/konamigx.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
},
|
||||
{
|
||||
"dest": "konamigv.zip",
|
||||
"sha1": "b4ff14ed8c3e37a4b45ce2d80c692961ffc88270",
|
||||
"sha256": "908384124008620fa81e0747c9984283416013754e7453368020881429eda189",
|
||||
"size": 81391,
|
||||
"repo_path": "bios/Arcade/Arcade/konamigv.zip",
|
||||
"sha1": "02dcad570077ee8e7aefec944e91650b99205bae",
|
||||
"sha256": "5d2557d5bfa013fa41228b23d80cb0ccf8de88a356906bd42d6d3d92cdbb147e",
|
||||
"size": 81413,
|
||||
"repo_path": "bios/Arcade/MAME/konamigv.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
},
|
||||
{
|
||||
"dest": "kviper.zip",
|
||||
"sha1": "78c8e1c3c033b65758b7e53a9346b44d037fea7f",
|
||||
"sha256": "d746ea1605f08994141e38277277953e55e202964b56cf6db4c277242c39f045",
|
||||
"size": 33864,
|
||||
"repo_path": "bios/Arcade/Arcade/kviper.zip",
|
||||
"sha1": "413371bb5f59976608debf83e69a1d0cb2b606b7",
|
||||
"sha256": "323f86555c5bd538825c5c9945bc7fb467eac0f3954dce55f6f6b8e8965cc877",
|
||||
"size": 33886,
|
||||
"repo_path": "bios/Arcade/MAME/kviper.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
},
|
||||
{
|
||||
"dest": "gq863.zip",
|
||||
"sha1": "975fbbbec0b578a495a8da9e1a5965d94d6d52e1",
|
||||
"sha256": "ba9aa024a73f9b45000d179e03b8b135f8a90371ba3bbe3722e3b4ae1f1b8045",
|
||||
"size": 74462,
|
||||
"repo_path": "bios/Arcade/Arcade/gq863.zip",
|
||||
"sha1": "f216c17e3191b1c8acb36568b0a2f983b5b7fea1",
|
||||
"sha256": "9a65179ed2bd33dafb47e4699b0ddecf7aa48dc2229746e989c227e750780628",
|
||||
"size": 74484,
|
||||
"repo_path": "bios/Arcade/MAME/gq863.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
@@ -2172,100 +2172,100 @@
|
||||
},
|
||||
{
|
||||
"dest": "coh1000c.zip",
|
||||
"sha1": "9e156eee236b0077a50a2cda1ff0b874390ed8db",
|
||||
"sha256": "8411e2b6e41326d2f4b6687b445155681a82246ad2746568e5026fcd7cf58466",
|
||||
"size": 278470,
|
||||
"repo_path": "bios/Arcade/Arcade/coh1000c.zip",
|
||||
"sha1": "67520b1be708b785efa26aa2bbc6b2180e0d191e",
|
||||
"sha256": "df486f9b603b068b531c00ccb7bfed329943e3c15a4e6fedde14ec9dc6c07801",
|
||||
"size": 278492,
|
||||
"repo_path": "bios/Arcade/MAME/coh1000c.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
},
|
||||
{
|
||||
"dest": "coh3002c.zip",
|
||||
"sha1": "94d49033e2c45a2591331826559bc93b2bb0884f",
|
||||
"sha256": "a85cdfb3d5c2d0ba2ee537776ba94ef9d69dc3a2f9d7217d6404fbb00563f666",
|
||||
"size": 137941,
|
||||
"repo_path": "bios/Arcade/Arcade/coh3002c.zip",
|
||||
"sha1": "9d9a06c4a9617396679111182fb85f1b116c4430",
|
||||
"sha256": "b54984e34f316e4e41de775f991e48beff9289bd30493345767465cfe7ff2a56",
|
||||
"size": 137963,
|
||||
"repo_path": "bios/Arcade/MAME/coh3002c.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
},
|
||||
{
|
||||
"dest": "coh1000t.zip",
|
||||
"sha1": "c6d884d7b7ea46338efd116d6ddfa1392c624649",
|
||||
"sha256": "d415464f70fdf2474fda692345729d77d2d35fe682cb8fbfc5999eb8c2765bee",
|
||||
"size": 126274,
|
||||
"repo_path": "bios/Arcade/Arcade/coh1000t.zip",
|
||||
"sha1": "87d1a6aa8733e4fd54e820348a060f98ba2fffc5",
|
||||
"sha256": "576762d85d5335a4966d9cbb53210c226c2f67d77aa25c3ec9c51862b2007492",
|
||||
"size": 126296,
|
||||
"repo_path": "bios/Arcade/MAME/coh1000t.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
},
|
||||
{
|
||||
"dest": "coh1000a.zip",
|
||||
"sha1": "c91f79c7ac73519c8b99c184b2fcbffae42fa324",
|
||||
"sha256": "7f4a45107f6eb3fcfe7078f9f2b2e5bbce1d3eff0b0e1fba84da068a69bdac8f",
|
||||
"size": 126259,
|
||||
"repo_path": "bios/Arcade/Arcade/coh1000a.zip",
|
||||
"sha1": "f8526dcec63402d2533d8180e217fa03a6322c34",
|
||||
"sha256": "64bec99b727a1c0b2970b9e9d61378cbe6834262e9857b06ea3c364676c29468",
|
||||
"size": 126281,
|
||||
"repo_path": "bios/Arcade/MAME/coh1000a.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
},
|
||||
{
|
||||
"dest": "coh1000w.zip",
|
||||
"sha1": "13bf28e3528c385b642046c2e0f96f44b33df0f2",
|
||||
"sha256": "5fae53908dcf09d5de4d0d5d6efd8f7315ea6f52cc224caba4cf888448202b73",
|
||||
"size": 126147,
|
||||
"repo_path": "bios/Arcade/Arcade/coh1000w.zip",
|
||||
"sha1": "bce86d35e38951c05e9299aa77d71b0557661eae",
|
||||
"sha256": "cbda93a154963f5f4c2d7b7fb3588011af300873400f0c46db80332fc8965a8e",
|
||||
"size": 126169,
|
||||
"repo_path": "bios/Arcade/MAME/coh1000w.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
},
|
||||
{
|
||||
"dest": "coh1001l.zip",
|
||||
"sha1": "ab30bd19261c208e2c44c35de6f68cd415fb80e9",
|
||||
"sha256": "2d49c0d3039fcd1b5ead9a166b4bc07d5b9ad17287547fc1aa4492d6d7cada2d",
|
||||
"size": 126185,
|
||||
"repo_path": "bios/Arcade/Arcade/coh1001l.zip",
|
||||
"sha1": "12983dbd03cd4787f850a2c39cabd4cfedfdc362",
|
||||
"sha256": "18ac64ca030d50a5edc1801181d2f55cf1d24b0861ab4136dee904a4f1876ce8",
|
||||
"size": 126207,
|
||||
"repo_path": "bios/Arcade/MAME/coh1001l.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
},
|
||||
{
|
||||
"dest": "coh1002v.zip",
|
||||
"sha1": "5162fcac2fa5a92480a081b3397099ce4a3703c5",
|
||||
"sha256": "9e4fb8208d357e7d0269628be6d2916d7031d248904ed9b6fb34a814432ae5f8",
|
||||
"size": 126314,
|
||||
"repo_path": "bios/Arcade/Arcade/coh1002v.zip",
|
||||
"sha1": "470da2042487c17fce84d8d10fc5af46b4594c9c",
|
||||
"sha256": "c3c79b83ddcfbc3e98e87f4bc0f23876b51548181633bc4cd15bafa06bfa67ea",
|
||||
"size": 126336,
|
||||
"repo_path": "bios/Arcade/MAME/coh1002v.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
},
|
||||
{
|
||||
"dest": "coh1002e.zip",
|
||||
"sha1": "1f15b3acc75031633387b372ea71e537f4f96220",
|
||||
"sha256": "31d0823e4ac37f1e7c8a5d9e98e556b74001c8c34e92fcd6315af3b0852f1ff9",
|
||||
"size": 128626,
|
||||
"repo_path": "bios/Arcade/Arcade/coh1002e.zip",
|
||||
"sha1": "670bd7592f4eef153223e7906ead8b2d97901291",
|
||||
"sha256": "36b81a13e73bb4d1e60fc9477321df5a32df7f13b806da9960bdeedd58909adf",
|
||||
"size": 128648,
|
||||
"repo_path": "bios/Arcade/MAME/coh1002e.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
},
|
||||
{
|
||||
"dest": "coh1002m.zip",
|
||||
"sha1": "197337577f55ab4c237c3d545d63082e73748c47",
|
||||
"sha256": "ac3c371340786919a9a420a7d0e6355709c3c5af62383e5c9ba3f90231af4723",
|
||||
"size": 138588,
|
||||
"repo_path": "bios/Arcade/Arcade/coh1002m.zip",
|
||||
"sha1": "88d002262d7c26983416d5bc90f039f93615f947",
|
||||
"sha256": "36c35f78a6a8978a2e734374445d145981205a1939b9cbb4a7aa1aefa1b4aaf8",
|
||||
"size": 138610,
|
||||
"repo_path": "bios/Arcade/MAME/coh1002m.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
},
|
||||
{
|
||||
"dest": "coh3002t.zip",
|
||||
"sha1": "2c4f6d8d134a2e096e676821fad0a66ac32e6e5c",
|
||||
"sha256": "18514d40206bf2f221a704b8853941111aae7e69e87a393ca6500de65716e523",
|
||||
"size": 504337,
|
||||
"repo_path": "bios/Arcade/Arcade/coh3002t.zip",
|
||||
"sha1": "119fbb7632b13834f36e7da45a22b662f52ffe1a",
|
||||
"sha256": "fa789b062b2bf065c2fa981f66820aef147bbf66702604e0c151597d4c551ebe",
|
||||
"size": 504359,
|
||||
"repo_path": "bios/Arcade/MAME/coh3002t.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
@@ -2282,20 +2282,20 @@
|
||||
},
|
||||
{
|
||||
"dest": "taitotz.zip",
|
||||
"sha1": "d7ccc6ea8b21f5178389543f5e14c06e4e6f3b57",
|
||||
"sha256": "80593cf39c7716fb07d8bf7341cad517f7e2ec76168ad4c6c9cc2750ebe3f9f6",
|
||||
"size": 77002,
|
||||
"repo_path": "bios/Arcade/Arcade/taitotz.zip",
|
||||
"sha1": "03e5703453dbca0c5ea4689b31524fea19eff34a",
|
||||
"sha256": "b008b4879c128bb4c7afe7eaf7dc51fff30d8309f51653118c5a7e2b795efb7a",
|
||||
"size": 77024,
|
||||
"repo_path": "bios/Arcade/MAME/taitotz.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
},
|
||||
{
|
||||
"dest": "skns.zip",
|
||||
"sha1": "4257bd14b541fafbd555cb98ba079a3416a45934",
|
||||
"sha256": "6edf22c68e70be06c9b32741e0d1df1aa38cf6fc162c1027c15d6c1d882f621b",
|
||||
"size": 924762,
|
||||
"repo_path": "bios/Arcade/Arcade/skns.zip",
|
||||
"sha1": "a8238391ddca35f9e59e5ca7f4f4491ae281b77f",
|
||||
"sha256": "51b5ed3a802b10d599c9e2e00ef38ed1e3caa534067aa810fe0cad094324add8",
|
||||
"size": 924740,
|
||||
"repo_path": "bios/Arcade/MAME/skns.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
@@ -2312,10 +2312,10 @@
|
||||
},
|
||||
{
|
||||
"dest": "macsbios.zip",
|
||||
"sha1": "4e0202f8430cb4842184df7b5418e32620156c7b",
|
||||
"sha256": "5c95c92c134455bdaf5e26b0b52b3051092886e778ff003dcc990028a7afa49c",
|
||||
"size": 305290,
|
||||
"repo_path": "bios/Arcade/Arcade/macsbios.zip",
|
||||
"sha1": "b09a68693ce9280bfd91f51ca4a1f7359ba81853",
|
||||
"sha256": "9ad8e5f6764455a22ccc401687d87a3dacb04c3efb18d190f83093562e85bf5b",
|
||||
"size": 305312,
|
||||
"repo_path": "bios/Arcade/MAME/macsbios.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
@@ -2342,10 +2342,10 @@
|
||||
},
|
||||
{
|
||||
"dest": "hng64.zip",
|
||||
"sha1": "98b92ece93d3e160c446a56ac859a8311333e36c",
|
||||
"sha256": "6c5a6332a6dc48832e505a8ed2ed9946753ed2d7dc96a7b40e5efd2bb720de4e",
|
||||
"size": 41210,
|
||||
"repo_path": "bios/Arcade/Arcade/hng64.zip",
|
||||
"sha1": "fb0c36d69f66f4b10a895aa708ae37f826755257",
|
||||
"sha256": "f10a8a309f0547cc09cb5aec92d740ca7ad35db4aa6bb50b5241743851a51f69",
|
||||
"size": 41592,
|
||||
"repo_path": "bios/Arcade/MAME/hng64.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
@@ -2362,70 +2362,70 @@
|
||||
},
|
||||
{
|
||||
"dest": "alg_bios.zip",
|
||||
"sha1": "901e1927ebe87946e3e4bb6d0a825088a8a60490",
|
||||
"sha256": "c0c48b9718bffac3ce64dc5e393306c4571d6a148c34249f70ffe9282996b804",
|
||||
"size": 143125,
|
||||
"repo_path": "bios/Arcade/Arcade/alg_bios.zip",
|
||||
"sha1": "1344452b4b29aacc9fdffc83f5cd14c028e32cc7",
|
||||
"sha256": "adc925cdd66a25b4168d7a6569bc7feed97f7a6ecc79df0a246f3fc34f072de0",
|
||||
"size": 143147,
|
||||
"repo_path": "bios/Arcade/MAME/alg_bios.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
},
|
||||
{
|
||||
"dest": "atarisy1.zip",
|
||||
"sha1": "4fb1f00681d2540b476add2def2bbf07ca3e3993",
|
||||
"sha256": "1cf61e6b54589ed9bf2b5f319c2b56de205384d6726b7a0fae79213071f3cfb5",
|
||||
"size": 49385,
|
||||
"repo_path": "bios/Arcade/Arcade/atarisy1.zip",
|
||||
"sha1": "0c9bc4b948bd1816ebe13e2acb97931cbcc6ea15",
|
||||
"sha256": "ae8c41b3d0107c6f0634730267af569a51e49f5df1bdb2c1e6c591bb6251b7d3",
|
||||
"size": 57375,
|
||||
"repo_path": "bios/Arcade/MAME/atarisy1.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
},
|
||||
{
|
||||
"dest": "maxaflex.zip",
|
||||
"sha1": "697551fcf9557ae33e31096b118a0c6769700a2e",
|
||||
"sha256": "2c139efc1d0b2ff22a316fbfd6a705c21717dfadc74d159e678e98d9a4b9f903",
|
||||
"size": 13673,
|
||||
"repo_path": "bios/Arcade/Arcade/maxaflex.zip",
|
||||
"sha1": "8542b67124c552629ec3b4ebca5035d8db64b799",
|
||||
"sha256": "e6860e2b3eb43a7993b3a5030e92d202958d285d7a4fbefbb4001e2ce579d0bc",
|
||||
"size": 13695,
|
||||
"repo_path": "bios/Arcade/MAME/maxaflex.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
},
|
||||
{
|
||||
"dest": "crysbios.zip",
|
||||
"sha1": "7f2d1c4e00ce1824ff91459d8ff6b59b2fb4b746",
|
||||
"sha256": "78e2fd2f0d7bced8d982adaa2be4f4860789250712cfefda354267eda24d0be7",
|
||||
"size": 43480,
|
||||
"repo_path": "bios/Arcade/Arcade/crysbios.zip",
|
||||
"sha1": "6227dd5f5d7083d0aed95a544b64183787699386",
|
||||
"sha256": "0ba851362a00400291e265c876e383d5f46ca6699307731e61cd84483a7a5ae9",
|
||||
"size": 109693,
|
||||
"repo_path": "bios/Arcade/MAME/crysbios.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
},
|
||||
{
|
||||
"dest": "galgbios.zip",
|
||||
"sha1": "9ec50f79c5fd6eaddd88542aa18b6c5bb81a9ed4",
|
||||
"sha256": "0132a956caa5705177688d2ee454aaf919b9cc3ed1ee6c4ae331f006292b59f0",
|
||||
"size": 1345333,
|
||||
"repo_path": "bios/Arcade/Arcade/galgbios.zip",
|
||||
"sha1": "f138c8df3637b804a0e5f2007af583b01c338554",
|
||||
"sha256": "d87cc31aebc4694e9d776734572dbe8b45071a4888d913cb4baac4fc51175a1f",
|
||||
"size": 1345355,
|
||||
"repo_path": "bios/Arcade/MAME/galgbios.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
},
|
||||
{
|
||||
"dest": "aristmk5.zip",
|
||||
"sha1": "81011bee956427e015cbb91e5d93364b6dc300dc",
|
||||
"sha256": "fff946335cfc26315d4fa9460977bb9c0bb70ce18964f5a4ea7d1ccf18e7d04e",
|
||||
"size": 261435,
|
||||
"repo_path": "bios/Arcade/Arcade/aristmk5.zip",
|
||||
"sha1": "c128717ef439e5260485111c97439a0bef682264",
|
||||
"sha256": "daf2092d3b70607912d988fac9dcab420cf0f604c5913c89552c661483aedbdb",
|
||||
"size": 566233,
|
||||
"repo_path": "bios/Arcade/MAME/aristmk5.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
},
|
||||
{
|
||||
"dest": "aristmk6.zip",
|
||||
"sha1": "e5a16ec95d54dd3ced17edccfdda049ac2f8f86f",
|
||||
"sha256": "413ed514a52797abfeed7b9d93900bc2c49c8ea24ded12ac0cc4368bf053f56f",
|
||||
"size": 22239668,
|
||||
"repo_path": "bios/Arcade/Arcade/aristmk6.zip",
|
||||
"sha1": "c5c72aec2f70003d99da9c33858036543ec53dff",
|
||||
"sha256": "cc1a10b1b611b6b22093bd2c38cba8b05af279cb18b1517ae7b45c31b02658c2",
|
||||
"size": 41477458,
|
||||
"repo_path": "bios/Arcade/MAME/aristmk6.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
@@ -2442,30 +2442,30 @@
|
||||
},
|
||||
{
|
||||
"dest": "allied.zip",
|
||||
"sha1": "f44836072ec6ad9e5a7b46105985548181faab5b",
|
||||
"sha256": "eb522861656943cf9fe9121bb80cd00c911aa20d4608f54d0ecbb8a58ce2009b",
|
||||
"size": 2926,
|
||||
"repo_path": "bios/Arcade/Arcade/allied.zip",
|
||||
"sha1": "cbb8ee9e3988683e42a54508d85d883f36c4fb51",
|
||||
"sha256": "f21fa6a0d943f188cf76475c4a38d87000fb6766d2118fb291aeb576cf6d8c61",
|
||||
"size": 2948,
|
||||
"repo_path": "bios/Arcade/MAME/allied.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
},
|
||||
{
|
||||
"dest": "gp_110.zip",
|
||||
"sha1": "6fdec00172233095cebb085b26214648d1093ecb",
|
||||
"sha256": "6368d9a6d566b25ae135c6da9e106821397974300fecee9a4d89c41641895c51",
|
||||
"size": 3140,
|
||||
"repo_path": "bios/Arcade/Arcade/gp_110.zip",
|
||||
"sha1": "bc58cfdbafb52d4b8d77ed0448425b2037ec7d79",
|
||||
"sha256": "1a2b7f0b779404d63346df1a909432ce17fb1d0063af105dc602f02e0ec50129",
|
||||
"size": 3162,
|
||||
"repo_path": "bios/Arcade/MAME/gp_110.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
},
|
||||
{
|
||||
"dest": "gts1.zip",
|
||||
"sha1": "346cbf24af08f5bfd96e6a77abf27595477aace8",
|
||||
"sha256": "2c8d682fecf646a5526aa788a6bd5fffb12e005572eb6eab8155a035df2af079",
|
||||
"size": 3734,
|
||||
"repo_path": "bios/Arcade/Arcade/gts1.zip",
|
||||
"sha1": "d0a347bbf8d3d6996a025054fe84f16719cc871f",
|
||||
"sha256": "2054f0619231def0b5dca29e0e2662bb63e7365f66d6dd0b143bc6fa153ffa4a",
|
||||
"size": 3756,
|
||||
"repo_path": "bios/Arcade/MAME/gts1.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
@@ -2512,40 +2512,40 @@
|
||||
},
|
||||
{
|
||||
"dest": "iteagle.zip",
|
||||
"sha1": "7b5be2c65e378f736ee4689303349d80dc425f5f",
|
||||
"sha256": "a12f79362c78b69d0c789ff7ae9075d1b92c826e4bbda4a5573505a3ba54b624",
|
||||
"size": 3602204,
|
||||
"repo_path": "bios/Arcade/Arcade/iteagle.zip",
|
||||
"sha1": "3b95307e772c0db158c3ad3b6425b85364cee75a",
|
||||
"sha256": "96f057d36a28132e6ef66b2685a4d69ab1b04152e3c6dae8ccaa84e9b4e732e3",
|
||||
"size": 3602226,
|
||||
"repo_path": "bios/Arcade/MAME/iteagle.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
},
|
||||
{
|
||||
"dest": "su2000.zip",
|
||||
"sha1": "2e50b96a1d37e9407209e291d5f549233287ac77",
|
||||
"sha256": "ff7e0e14c59317c8e67795b790fa73dc951664d778b6f09041f3942a6c86e469",
|
||||
"size": 90729,
|
||||
"repo_path": "bios/Arcade/Arcade/su2000.zip",
|
||||
"sha1": "86f5074cf3c6353156ea3f1bb49bf840410cf0ea",
|
||||
"sha256": "6818ead4f9d0b28134c0b71f5697446bf55efe70b3e6c1ef138b6365a3230f30",
|
||||
"size": 90751,
|
||||
"repo_path": "bios/Arcade/MAME/su2000.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
},
|
||||
{
|
||||
"dest": "tourvis.zip",
|
||||
"sha1": "a1352d895578181532d13e81ac7a8fecad564d1a",
|
||||
"sha256": "7915bbe4e23fdaa5116622ff3c269def5368bb0008369f52d5cc966ab80789d2",
|
||||
"size": 29492,
|
||||
"repo_path": "bios/Arcade/Arcade/tourvis.zip",
|
||||
"sha1": "1382217486f524266a1f598d40e6e808209be5a8",
|
||||
"sha256": "b6cbcc04b5ff078c4ca22e4bcc1a012f1ff4a482c96d06964078b33580e9524a",
|
||||
"size": 69276,
|
||||
"repo_path": "bios/Arcade/MAME/tourvis.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
},
|
||||
{
|
||||
"dest": "v4bios.zip",
|
||||
"sha1": "5024b3bfd04ccd2061eb60d2eca254e8faf44f5c",
|
||||
"sha256": "29e05af157d01d146c7ec501e99b7931d78920beccf9b66c9b310c860941e37f",
|
||||
"size": 3753,
|
||||
"repo_path": "bios/Arcade/Arcade/v4bios.zip",
|
||||
"sha1": "9964f68aadd3eef4daa4c0d946422821b4352208",
|
||||
"sha256": "cf23bfd8a445d0888afbc198ca5edce049a70ce9f5f02a1561ee4bc29efdd1d9",
|
||||
"size": 3775,
|
||||
"repo_path": "bios/Arcade/MAME/v4bios.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
@@ -2562,10 +2562,10 @@
|
||||
},
|
||||
{
|
||||
"dest": "pumpitup.zip",
|
||||
"sha1": "6c9139fe4e094088029e545ab62520052971906a",
|
||||
"sha256": "9f86200a6e51e12bbb4526f1bd8c05fcd22112a21c04566582f732da8f750835",
|
||||
"size": 1838700,
|
||||
"repo_path": "bios/Arcade/Arcade/pumpitup.zip",
|
||||
"sha1": "a32161e3b5904f3eb87a156d35fbd941c8f913d0",
|
||||
"sha256": "4a9187a43ff9ccae171ecfed8284af57328d7ac645516bdb763b227b54b6ddc1",
|
||||
"size": 7985810,
|
||||
"repo_path": "bios/Arcade/MAME/pumpitup.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
@@ -2722,10 +2722,10 @@
|
||||
},
|
||||
{
|
||||
"dest": "adam_spi.zip",
|
||||
"sha1": "e7328f43ceea8c9c0a6dc814680da9ff46e93fd3",
|
||||
"sha256": "3ece02358b7292d5525d5b648412e7028cfa7cf670c6351c955bfc19351ce58c",
|
||||
"size": 446843,
|
||||
"repo_path": "bios/Arcade/Arcade/adam_spi.zip",
|
||||
"sha1": "f534c2cfc9729f06b13f87ce62fbd8f63731e160",
|
||||
"sha256": "a52e597afbb2dd19fd31a9bc51199d8811f4369ab8ab9a30cfb5c9a9c24309c2",
|
||||
"size": 576494,
|
||||
"repo_path": "bios/Arcade/MAME/adam_spi.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
@@ -3162,10 +3162,10 @@
|
||||
},
|
||||
{
|
||||
"dest": "qsound.zip",
|
||||
"sha1": "f1ccedb2c6e8f15cfebab52c3923e4b35f4df43f",
|
||||
"sha256": "de8cfe2c1a0207b02a937a1cde4c56040e6d5aa7d0f52c26652a35d9f07dd9b6",
|
||||
"size": 2654,
|
||||
"repo_path": "bios/Arcade/Arcade/qsound.zip",
|
||||
"sha1": "b875be47c087d7a0d484d29232a23fd0bae736e6",
|
||||
"sha256": "55feb9b70dacb7364f0310ed16cc9281b5b0a31318b9e2e0587ae06d15f95356",
|
||||
"size": 2676,
|
||||
"repo_path": "bios/Arcade/MAME/qsound.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
@@ -3182,10 +3182,10 @@
|
||||
},
|
||||
{
|
||||
"dest": "ym2608.zip",
|
||||
"sha1": "06fc753d015b43ca1787f4cfd9331b1674202e64",
|
||||
"sha256": "187ede0f57d1439092a7ac50ea923533871ab7ee65e29baabc4dcdfa7ad4823f",
|
||||
"size": 7609,
|
||||
"repo_path": "bios/Arcade/Arcade/ym2608.zip",
|
||||
"sha1": "1214e3207e4dc85cc1597c53abd70802049dda2c",
|
||||
"sha256": "817aabbfe1ed8734a6cbb88d72178a80b6074dea155cfe1c95f2e4eb8990d1be",
|
||||
"size": 7587,
|
||||
"repo_path": "bios/Arcade/MAME/ym2608.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
|
||||
+1533
-1563
File diff suppressed because it is too large.
Load diff
+505
-1835
File diff suppressed because it is too large.
Load diff
+197
-177
@@ -5,7 +5,7 @@
|
||||
"platform": "recalbox",
|
||||
"display_name": "Recalbox",
|
||||
"version": "1.0",
|
||||
"generated": "2026-10-05T00:24:12Z",
|
||||
"generated": "2026-10-05T22:37:20Z",
|
||||
"base_destination": "bios",
|
||||
"detect": [
|
||||
{
|
||||
@@ -237,10 +237,10 @@
|
||||
}
|
||||
}
|
||||
],
|
||||
"total_files": 3234,
|
||||
"total_size": 4176395667,
|
||||
"pack_files": 3236,
|
||||
"pack_size": 4176395667,
|
||||
"total_files": 3236,
|
||||
"total_size": 4205115053,
|
||||
"pack_files": 3238,
|
||||
"pack_size": 4205115053,
|
||||
"total_omitted": 4,
|
||||
"omitted_files": [
|
||||
{
|
||||
@@ -21409,10 +21409,10 @@
|
||||
},
|
||||
{
|
||||
"dest": "fbneo/hiscore.dat",
|
||||
"sha1": "7381472bf046126257e51a0124e4553282f020e5",
|
||||
"sha256": "9d44fc928c0df41debad1b86739bd6ca96a5891b3b499f6fb3d7e9ea95ca8de5",
|
||||
"size": 322458,
|
||||
"repo_path": "bios/Arcade/Arcade/hiscore.dat",
|
||||
"sha1": "ae3329abbcb7b445d979321a1f67733aa3f023d0",
|
||||
"sha256": "d9c51a13ec2c6db6c9ba0f955c8a8fd8fe54fc2ec05cd82226f4ddf53d500182",
|
||||
"size": 314122,
|
||||
"repo_path": "bios/Arcade/FBNeo/hiscore.dat",
|
||||
"cores": [
|
||||
"fbneo"
|
||||
]
|
||||
@@ -22075,40 +22075,40 @@
|
||||
},
|
||||
{
|
||||
"dest": "f355dlx.zip",
|
||||
"sha1": "48d1712d1b1cdfeeeb43c6287c17b0b6309cfaab",
|
||||
"sha256": "58e88cedb31918e2b9deeaddb80df45b8484cc1c4f10792520360a2585ad5014",
|
||||
"size": 2328436,
|
||||
"repo_path": "bios/Arcade/Arcade/f355dlx.zip",
|
||||
"sha1": "7740229e15dd80b20a8d263a67cd37c310602ee4",
|
||||
"sha256": "92d8634d8c61a1b50aa0b517638d8829bb70e7fe051423695722e77869d6780c",
|
||||
"size": 2331068,
|
||||
"repo_path": "bios/Arcade/MAME/f355dlx.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
},
|
||||
{
|
||||
"dest": "f355bios.zip",
|
||||
"sha1": "b6ff66dcb5547bd91760d239ddf428a655631c53",
|
||||
"sha256": "8335208b035253d5c671ecaaa113369409ffd70e86d1c87ab1e21bdf352fff6c",
|
||||
"size": 1394278,
|
||||
"repo_path": "bios/Arcade/Arcade/f355bios.zip",
|
||||
"sha1": "37487f9f73734e8dd621004a5ef98c5a2301bd1d",
|
||||
"sha256": "47ebe6dad9d8398c7eacb69c652d8f5c449c6f7ce420579c3a287c1b73e0250b",
|
||||
"size": 1396910,
|
||||
"repo_path": "bios/Arcade/MAME/f355bios.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
},
|
||||
{
|
||||
"dest": "lindbios.zip",
|
||||
"sha1": "65a2f2cee74c316d5f40b68deda66787609df353",
|
||||
"sha256": "9d057f11a2c4cfa7c637016fbca20c125c86a137ab4ca1957079f5cf0e73000c",
|
||||
"size": 1462941,
|
||||
"repo_path": "bios/Arcade/Arcade/lindbios.zip",
|
||||
"sha1": "cd2601242084856146bae2bea3084930b423e744",
|
||||
"sha256": "0fadf3027e8b690de567140c3ed72885da25c18471345e9adc18d3aa879a6859",
|
||||
"size": 1462963,
|
||||
"repo_path": "bios/Arcade/MAME/lindbios.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
},
|
||||
{
|
||||
"dest": "segasp.zip",
|
||||
"sha1": "de78a0b8df78d5ffdb1a274f80d877a96ef204aa",
|
||||
"sha256": "3bca348667d6ff08fd5efe38eec35100ccbe0de3cd1e0b5dd72ae5c6cc201ee9",
|
||||
"size": 1281424,
|
||||
"repo_path": "bios/Arcade/Arcade/segasp.zip",
|
||||
"sha1": "b4c7e46fb510096acf8fb19fb99c8c5a8ab1d9e1",
|
||||
"sha256": "bda96939992d42410034caf3f5296d8fc63eced1693b5b522536c6ac717ded73",
|
||||
"size": 4266446,
|
||||
"repo_path": "bios/Arcade/MAME/segasp.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
@@ -22125,10 +22125,10 @@
|
||||
},
|
||||
{
|
||||
"dest": "chihiro.zip",
|
||||
"sha1": "4fe94baf28d658dd2651de4055a0ac33aa42aa0e",
|
||||
"sha256": "760e30588b1d0fc7a3ede0097a0a5ca423c26b9e6a11c40d9e7f285489b57e4f",
|
||||
"size": 2104141,
|
||||
"repo_path": "bios/Arcade/Arcade/chihiro.zip",
|
||||
"sha1": "459832de5d758b76ac85a6c07b693496cd5ad7f2",
|
||||
"sha256": "48a268250decb2ff9e5388def44f66f2a260da002162677cc96d55892fafa6ad",
|
||||
"size": 2194504,
|
||||
"repo_path": "bios/Arcade/MAME/chihiro.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
@@ -22145,30 +22145,30 @@
|
||||
},
|
||||
{
|
||||
"dest": "shtzone.zip",
|
||||
"sha1": "2adbde3ef74c431fbd700cffd4cd1d41251a8833",
|
||||
"sha256": "123e8fa1550b12d7c1d07dfe5936d7bed3bb345abd3d1d5e02ad851df8167cd3",
|
||||
"size": 3332,
|
||||
"repo_path": "bios/Arcade/Arcade/shtzone.zip",
|
||||
"sha1": "d08a271feaab04ac904e6357c23bca95c1cf57f1",
|
||||
"sha256": "a62534c0fafce53482946d7d1b72ca844979c4e77a892264e3038a5ad87ba658",
|
||||
"size": 3354,
|
||||
"repo_path": "bios/Arcade/MAME/shtzone.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
},
|
||||
{
|
||||
"dest": "sammymdl.zip",
|
||||
"sha1": "2c178083ac2906cecb7e331d7111f7766a353052",
|
||||
"sha256": "b624c087452f580d19629105c55fd322ca2474f3153c8e5d366a8cedc0eecf66",
|
||||
"size": 24090,
|
||||
"repo_path": "bios/Arcade/Arcade/sammymdl.zip",
|
||||
"sha1": "a2d1cfe34d5e62b7c8d6490383fb079ffe7f4a4a",
|
||||
"sha256": "9767a4757761956c220df202e6cce2821aeb57b1daef544ee233ae7fd5fa5bad",
|
||||
"size": 24112,
|
||||
"repo_path": "bios/Arcade/MAME/sammymdl.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
},
|
||||
{
|
||||
"dest": "isgsm.zip",
|
||||
"sha1": "f590ccf688b4c05fa1da5c5dd92c224545170c3b",
|
||||
"sha256": "c481bfde0ae68e5fbe70a406af1698fcba5b76d7e23c860c2715cacf019efad4",
|
||||
"size": 10207,
|
||||
"repo_path": "bios/Arcade/Arcade/isgsm.zip",
|
||||
"sha1": "6a19d46d3915f2bff823ac34327bb0976fb2fed7",
|
||||
"sha256": "2a7a1a7ee15b322356bcdfd46dd3592bcf6a9bf6cb04f4cd2cf27ea54dbb6665",
|
||||
"size": 10185,
|
||||
"repo_path": "bios/Arcade/MAME/isgsm.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
@@ -22185,20 +22185,20 @@
|
||||
},
|
||||
{
|
||||
"dest": "sfcbox.zip",
|
||||
"sha1": "14311945b06a5d013d1df5ce961ab47fbd46fb2a",
|
||||
"sha256": "75e95fb7431568fe4e71c0d71bbf41ebce3091a2d25ecc8da53f7eb115c4eba8",
|
||||
"size": 194344,
|
||||
"repo_path": "bios/Arcade/Arcade/sfcbox.zip",
|
||||
"sha1": "21194d81d1a27dbf688ea43cf79d312e4fe2d0ee",
|
||||
"sha256": "bb09a26b65fcc2d3217f9b0c8b7552de4df44f2f792d97ca7845114e464c8f50",
|
||||
"size": 242698,
|
||||
"repo_path": "bios/Arcade/MAME/sfcbox.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
},
|
||||
{
|
||||
"dest": "aleck64.zip",
|
||||
"sha1": "beaf97c4a0e0792b8db65648f9dabb6a54ae0549",
|
||||
"sha256": "53e9cb3814cfa086a4cb152d250a740d2a438aaadbeec248a86a1698e69dc24a",
|
||||
"size": 1804,
|
||||
"repo_path": "bios/Arcade/Arcade/aleck64.zip",
|
||||
"sha1": "6f2f76832d6dc2c18997421ba26d22d623715c77",
|
||||
"sha256": "9785ce8820981de663ad9114c11ed0b70f548c80c4eb699b56745ddc3d106673",
|
||||
"size": 1826,
|
||||
"repo_path": "bios/Arcade/MAME/aleck64.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
@@ -22215,20 +22215,20 @@
|
||||
},
|
||||
{
|
||||
"dest": "sys246.zip",
|
||||
"sha1": "fbba87e9d6852862252d31dd919f943c8edd7eb7",
|
||||
"sha256": "f3bb5f2101534172ec49f6e7b6eb66bb2a2116c953d64517fc098a51f042002a",
|
||||
"size": 580122,
|
||||
"repo_path": "bios/Arcade/Arcade/sys246.zip",
|
||||
"sha1": "b7452e0120830f0770c097d6a4ef94d61659e3e9",
|
||||
"sha256": "923f6dffdd94b496d47c4b065884eef820e66c7d9ad7ebc759bffcef1e571c1e",
|
||||
"size": 580144,
|
||||
"repo_path": "bios/Arcade/MAME/sys246.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
},
|
||||
{
|
||||
"dest": "sys256.zip",
|
||||
"sha1": "721621a353051fea6de484c26b534cdec4de616e",
|
||||
"sha256": "5fc6a17d793b5b647c7590f650ee3f21c5210cca7e05e51734a3c3d20182698d",
|
||||
"size": 582110,
|
||||
"repo_path": "bios/Arcade/Arcade/sys256.zip",
|
||||
"sha1": "481e24901892848d005e5d9466d57ab3432b9b29",
|
||||
"sha256": "dca31e0839b94f0bf9d83ced74e4eb5612a5aed12df82d036f5a8c2acdaf3a5b",
|
||||
"size": 582132,
|
||||
"repo_path": "bios/Arcade/MAME/sys256.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
@@ -22245,20 +22245,20 @@
|
||||
},
|
||||
{
|
||||
"dest": "kviper.zip",
|
||||
"sha1": "78c8e1c3c033b65758b7e53a9346b44d037fea7f",
|
||||
"sha256": "d746ea1605f08994141e38277277953e55e202964b56cf6db4c277242c39f045",
|
||||
"size": 33864,
|
||||
"repo_path": "bios/Arcade/Arcade/kviper.zip",
|
||||
"sha1": "413371bb5f59976608debf83e69a1d0cb2b606b7",
|
||||
"sha256": "323f86555c5bd538825c5c9945bc7fb467eac0f3954dce55f6f6b8e8965cc877",
|
||||
"size": 33886,
|
||||
"repo_path": "bios/Arcade/MAME/kviper.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
},
|
||||
{
|
||||
"dest": "gq863.zip",
|
||||
"sha1": "975fbbbec0b578a495a8da9e1a5965d94d6d52e1",
|
||||
"sha256": "ba9aa024a73f9b45000d179e03b8b135f8a90371ba3bbe3722e3b4ae1f1b8045",
|
||||
"size": 74462,
|
||||
"repo_path": "bios/Arcade/Arcade/gq863.zip",
|
||||
"sha1": "f216c17e3191b1c8acb36568b0a2f983b5b7fea1",
|
||||
"sha256": "9a65179ed2bd33dafb47e4699b0ddecf7aa48dc2229746e989c227e750780628",
|
||||
"size": 74484,
|
||||
"repo_path": "bios/Arcade/MAME/gq863.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
@@ -22305,100 +22305,100 @@
|
||||
},
|
||||
{
|
||||
"dest": "coh1000c.zip",
|
||||
"sha1": "9e156eee236b0077a50a2cda1ff0b874390ed8db",
|
||||
"sha256": "8411e2b6e41326d2f4b6687b445155681a82246ad2746568e5026fcd7cf58466",
|
||||
"size": 278470,
|
||||
"repo_path": "bios/Arcade/Arcade/coh1000c.zip",
|
||||
"sha1": "67520b1be708b785efa26aa2bbc6b2180e0d191e",
|
||||
"sha256": "df486f9b603b068b531c00ccb7bfed329943e3c15a4e6fedde14ec9dc6c07801",
|
||||
"size": 278492,
|
||||
"repo_path": "bios/Arcade/MAME/coh1000c.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
},
|
||||
{
|
||||
"dest": "coh3002c.zip",
|
||||
"sha1": "94d49033e2c45a2591331826559bc93b2bb0884f",
|
||||
"sha256": "a85cdfb3d5c2d0ba2ee537776ba94ef9d69dc3a2f9d7217d6404fbb00563f666",
|
||||
"size": 137941,
|
||||
"repo_path": "bios/Arcade/Arcade/coh3002c.zip",
|
||||
"sha1": "9d9a06c4a9617396679111182fb85f1b116c4430",
|
||||
"sha256": "b54984e34f316e4e41de775f991e48beff9289bd30493345767465cfe7ff2a56",
|
||||
"size": 137963,
|
||||
"repo_path": "bios/Arcade/MAME/coh3002c.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
},
|
||||
{
|
||||
"dest": "coh1000t.zip",
|
||||
"sha1": "c6d884d7b7ea46338efd116d6ddfa1392c624649",
|
||||
"sha256": "d415464f70fdf2474fda692345729d77d2d35fe682cb8fbfc5999eb8c2765bee",
|
||||
"size": 126274,
|
||||
"repo_path": "bios/Arcade/Arcade/coh1000t.zip",
|
||||
"sha1": "87d1a6aa8733e4fd54e820348a060f98ba2fffc5",
|
||||
"sha256": "576762d85d5335a4966d9cbb53210c226c2f67d77aa25c3ec9c51862b2007492",
|
||||
"size": 126296,
|
||||
"repo_path": "bios/Arcade/MAME/coh1000t.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
},
|
||||
{
|
||||
"dest": "coh1000a.zip",
|
||||
"sha1": "c91f79c7ac73519c8b99c184b2fcbffae42fa324",
|
||||
"sha256": "7f4a45107f6eb3fcfe7078f9f2b2e5bbce1d3eff0b0e1fba84da068a69bdac8f",
|
||||
"size": 126259,
|
||||
"repo_path": "bios/Arcade/Arcade/coh1000a.zip",
|
||||
"sha1": "f8526dcec63402d2533d8180e217fa03a6322c34",
|
||||
"sha256": "64bec99b727a1c0b2970b9e9d61378cbe6834262e9857b06ea3c364676c29468",
|
||||
"size": 126281,
|
||||
"repo_path": "bios/Arcade/MAME/coh1000a.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
},
|
||||
{
|
||||
"dest": "coh1000w.zip",
|
||||
"sha1": "13bf28e3528c385b642046c2e0f96f44b33df0f2",
|
||||
"sha256": "5fae53908dcf09d5de4d0d5d6efd8f7315ea6f52cc224caba4cf888448202b73",
|
||||
"size": 126147,
|
||||
"repo_path": "bios/Arcade/Arcade/coh1000w.zip",
|
||||
"sha1": "bce86d35e38951c05e9299aa77d71b0557661eae",
|
||||
"sha256": "cbda93a154963f5f4c2d7b7fb3588011af300873400f0c46db80332fc8965a8e",
|
||||
"size": 126169,
|
||||
"repo_path": "bios/Arcade/MAME/coh1000w.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
},
|
||||
{
|
||||
"dest": "coh1001l.zip",
|
||||
"sha1": "ab30bd19261c208e2c44c35de6f68cd415fb80e9",
|
||||
"sha256": "2d49c0d3039fcd1b5ead9a166b4bc07d5b9ad17287547fc1aa4492d6d7cada2d",
|
||||
"size": 126185,
|
||||
"repo_path": "bios/Arcade/Arcade/coh1001l.zip",
|
||||
"sha1": "12983dbd03cd4787f850a2c39cabd4cfedfdc362",
|
||||
"sha256": "18ac64ca030d50a5edc1801181d2f55cf1d24b0861ab4136dee904a4f1876ce8",
|
||||
"size": 126207,
|
||||
"repo_path": "bios/Arcade/MAME/coh1001l.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
},
|
||||
{
|
||||
"dest": "coh1002v.zip",
|
||||
"sha1": "5162fcac2fa5a92480a081b3397099ce4a3703c5",
|
||||
"sha256": "9e4fb8208d357e7d0269628be6d2916d7031d248904ed9b6fb34a814432ae5f8",
|
||||
"size": 126314,
|
||||
"repo_path": "bios/Arcade/Arcade/coh1002v.zip",
|
||||
"sha1": "470da2042487c17fce84d8d10fc5af46b4594c9c",
|
||||
"sha256": "c3c79b83ddcfbc3e98e87f4bc0f23876b51548181633bc4cd15bafa06bfa67ea",
|
||||
"size": 126336,
|
||||
"repo_path": "bios/Arcade/MAME/coh1002v.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
},
|
||||
{
|
||||
"dest": "coh1002e.zip",
|
||||
"sha1": "1f15b3acc75031633387b372ea71e537f4f96220",
|
||||
"sha256": "31d0823e4ac37f1e7c8a5d9e98e556b74001c8c34e92fcd6315af3b0852f1ff9",
|
||||
"size": 128626,
|
||||
"repo_path": "bios/Arcade/Arcade/coh1002e.zip",
|
||||
"sha1": "670bd7592f4eef153223e7906ead8b2d97901291",
|
||||
"sha256": "36b81a13e73bb4d1e60fc9477321df5a32df7f13b806da9960bdeedd58909adf",
|
||||
"size": 128648,
|
||||
"repo_path": "bios/Arcade/MAME/coh1002e.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
},
|
||||
{
|
||||
"dest": "coh1002m.zip",
|
||||
"sha1": "197337577f55ab4c237c3d545d63082e73748c47",
|
||||
"sha256": "ac3c371340786919a9a420a7d0e6355709c3c5af62383e5c9ba3f90231af4723",
|
||||
"size": 138588,
|
||||
"repo_path": "bios/Arcade/Arcade/coh1002m.zip",
|
||||
"sha1": "88d002262d7c26983416d5bc90f039f93615f947",
|
||||
"sha256": "36c35f78a6a8978a2e734374445d145981205a1939b9cbb4a7aa1aefa1b4aaf8",
|
||||
"size": 138610,
|
||||
"repo_path": "bios/Arcade/MAME/coh1002m.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
},
|
||||
{
|
||||
"dest": "coh3002t.zip",
|
||||
"sha1": "2c4f6d8d134a2e096e676821fad0a66ac32e6e5c",
|
||||
"sha256": "18514d40206bf2f221a704b8853941111aae7e69e87a393ca6500de65716e523",
|
||||
"size": 504337,
|
||||
"repo_path": "bios/Arcade/Arcade/coh3002t.zip",
|
||||
"sha1": "119fbb7632b13834f36e7da45a22b662f52ffe1a",
|
||||
"sha256": "fa789b062b2bf065c2fa981f66820aef147bbf66702604e0c151597d4c551ebe",
|
||||
"size": 504359,
|
||||
"repo_path": "bios/Arcade/MAME/coh3002t.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
@@ -22415,10 +22415,10 @@
|
||||
},
|
||||
{
|
||||
"dest": "taitotz.zip",
|
||||
"sha1": "d7ccc6ea8b21f5178389543f5e14c06e4e6f3b57",
|
||||
"sha256": "80593cf39c7716fb07d8bf7341cad517f7e2ec76168ad4c6c9cc2750ebe3f9f6",
|
||||
"size": 77002,
|
||||
"repo_path": "bios/Arcade/Arcade/taitotz.zip",
|
||||
"sha1": "03e5703453dbca0c5ea4689b31524fea19eff34a",
|
||||
"sha256": "b008b4879c128bb4c7afe7eaf7dc51fff30d8309f51653118c5a7e2b795efb7a",
|
||||
"size": 77024,
|
||||
"repo_path": "bios/Arcade/MAME/taitotz.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
@@ -22455,50 +22455,50 @@
|
||||
},
|
||||
{
|
||||
"dest": "alg_bios.zip",
|
||||
"sha1": "901e1927ebe87946e3e4bb6d0a825088a8a60490",
|
||||
"sha256": "c0c48b9718bffac3ce64dc5e393306c4571d6a148c34249f70ffe9282996b804",
|
||||
"size": 143125,
|
||||
"repo_path": "bios/Arcade/Arcade/alg_bios.zip",
|
||||
"sha1": "1344452b4b29aacc9fdffc83f5cd14c028e32cc7",
|
||||
"sha256": "adc925cdd66a25b4168d7a6569bc7feed97f7a6ecc79df0a246f3fc34f072de0",
|
||||
"size": 143147,
|
||||
"repo_path": "bios/Arcade/MAME/alg_bios.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
},
|
||||
{
|
||||
"dest": "atarisy1.zip",
|
||||
"sha1": "4fb1f00681d2540b476add2def2bbf07ca3e3993",
|
||||
"sha256": "1cf61e6b54589ed9bf2b5f319c2b56de205384d6726b7a0fae79213071f3cfb5",
|
||||
"size": 49385,
|
||||
"repo_path": "bios/Arcade/Arcade/atarisy1.zip",
|
||||
"sha1": "0c9bc4b948bd1816ebe13e2acb97931cbcc6ea15",
|
||||
"sha256": "ae8c41b3d0107c6f0634730267af569a51e49f5df1bdb2c1e6c591bb6251b7d3",
|
||||
"size": 57375,
|
||||
"repo_path": "bios/Arcade/MAME/atarisy1.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
},
|
||||
{
|
||||
"dest": "galgbios.zip",
|
||||
"sha1": "9ec50f79c5fd6eaddd88542aa18b6c5bb81a9ed4",
|
||||
"sha256": "0132a956caa5705177688d2ee454aaf919b9cc3ed1ee6c4ae331f006292b59f0",
|
||||
"size": 1345333,
|
||||
"repo_path": "bios/Arcade/Arcade/galgbios.zip",
|
||||
"sha1": "f138c8df3637b804a0e5f2007af583b01c338554",
|
||||
"sha256": "d87cc31aebc4694e9d776734572dbe8b45071a4888d913cb4baac4fc51175a1f",
|
||||
"size": 1345355,
|
||||
"repo_path": "bios/Arcade/MAME/galgbios.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
},
|
||||
{
|
||||
"dest": "aristmk5.zip",
|
||||
"sha1": "81011bee956427e015cbb91e5d93364b6dc300dc",
|
||||
"sha256": "fff946335cfc26315d4fa9460977bb9c0bb70ce18964f5a4ea7d1ccf18e7d04e",
|
||||
"size": 261435,
|
||||
"repo_path": "bios/Arcade/Arcade/aristmk5.zip",
|
||||
"sha1": "c128717ef439e5260485111c97439a0bef682264",
|
||||
"sha256": "daf2092d3b70607912d988fac9dcab420cf0f604c5913c89552c661483aedbdb",
|
||||
"size": 566233,
|
||||
"repo_path": "bios/Arcade/MAME/aristmk5.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
},
|
||||
{
|
||||
"dest": "aristmk6.zip",
|
||||
"sha1": "e5a16ec95d54dd3ced17edccfdda049ac2f8f86f",
|
||||
"sha256": "413ed514a52797abfeed7b9d93900bc2c49c8ea24ded12ac0cc4368bf053f56f",
|
||||
"size": 22239668,
|
||||
"repo_path": "bios/Arcade/Arcade/aristmk6.zip",
|
||||
"sha1": "c5c72aec2f70003d99da9c33858036543ec53dff",
|
||||
"sha256": "cc1a10b1b611b6b22093bd2c38cba8b05af279cb18b1517ae7b45c31b02658c2",
|
||||
"size": 41477458,
|
||||
"repo_path": "bios/Arcade/MAME/aristmk6.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
@@ -22515,30 +22515,30 @@
|
||||
},
|
||||
{
|
||||
"dest": "allied.zip",
|
||||
"sha1": "f44836072ec6ad9e5a7b46105985548181faab5b",
|
||||
"sha256": "eb522861656943cf9fe9121bb80cd00c911aa20d4608f54d0ecbb8a58ce2009b",
|
||||
"size": 2926,
|
||||
"repo_path": "bios/Arcade/Arcade/allied.zip",
|
||||
"sha1": "cbb8ee9e3988683e42a54508d85d883f36c4fb51",
|
||||
"sha256": "f21fa6a0d943f188cf76475c4a38d87000fb6766d2118fb291aeb576cf6d8c61",
|
||||
"size": 2948,
|
||||
"repo_path": "bios/Arcade/MAME/allied.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
},
|
||||
{
|
||||
"dest": "gp_110.zip",
|
||||
"sha1": "6fdec00172233095cebb085b26214648d1093ecb",
|
||||
"sha256": "6368d9a6d566b25ae135c6da9e106821397974300fecee9a4d89c41641895c51",
|
||||
"size": 3140,
|
||||
"repo_path": "bios/Arcade/Arcade/gp_110.zip",
|
||||
"sha1": "bc58cfdbafb52d4b8d77ed0448425b2037ec7d79",
|
||||
"sha256": "1a2b7f0b779404d63346df1a909432ce17fb1d0063af105dc602f02e0ec50129",
|
||||
"size": 3162,
|
||||
"repo_path": "bios/Arcade/MAME/gp_110.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
},
|
||||
{
|
||||
"dest": "gts1.zip",
|
||||
"sha1": "346cbf24af08f5bfd96e6a77abf27595477aace8",
|
||||
"sha256": "2c8d682fecf646a5526aa788a6bd5fffb12e005572eb6eab8155a035df2af079",
|
||||
"size": 3734,
|
||||
"repo_path": "bios/Arcade/Arcade/gts1.zip",
|
||||
"sha1": "d0a347bbf8d3d6996a025054fe84f16719cc871f",
|
||||
"sha256": "2054f0619231def0b5dca29e0e2662bb63e7365f66d6dd0b143bc6fa153ffa4a",
|
||||
"size": 3756,
|
||||
"repo_path": "bios/Arcade/MAME/gts1.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
@@ -22585,40 +22585,40 @@
|
||||
},
|
||||
{
|
||||
"dest": "iteagle.zip",
|
||||
"sha1": "7b5be2c65e378f736ee4689303349d80dc425f5f",
|
||||
"sha256": "a12f79362c78b69d0c789ff7ae9075d1b92c826e4bbda4a5573505a3ba54b624",
|
||||
"size": 3602204,
|
||||
"repo_path": "bios/Arcade/Arcade/iteagle.zip",
|
||||
"sha1": "3b95307e772c0db158c3ad3b6425b85364cee75a",
|
||||
"sha256": "96f057d36a28132e6ef66b2685a4d69ab1b04152e3c6dae8ccaa84e9b4e732e3",
|
||||
"size": 3602226,
|
||||
"repo_path": "bios/Arcade/MAME/iteagle.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
},
|
||||
{
|
||||
"dest": "su2000.zip",
|
||||
"sha1": "2e50b96a1d37e9407209e291d5f549233287ac77",
|
||||
"sha256": "ff7e0e14c59317c8e67795b790fa73dc951664d778b6f09041f3942a6c86e469",
|
||||
"size": 90729,
|
||||
"repo_path": "bios/Arcade/Arcade/su2000.zip",
|
||||
"sha1": "86f5074cf3c6353156ea3f1bb49bf840410cf0ea",
|
||||
"sha256": "6818ead4f9d0b28134c0b71f5697446bf55efe70b3e6c1ef138b6365a3230f30",
|
||||
"size": 90751,
|
||||
"repo_path": "bios/Arcade/MAME/su2000.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
},
|
||||
{
|
||||
"dest": "tourvis.zip",
|
||||
"sha1": "a1352d895578181532d13e81ac7a8fecad564d1a",
|
||||
"sha256": "7915bbe4e23fdaa5116622ff3c269def5368bb0008369f52d5cc966ab80789d2",
|
||||
"size": 29492,
|
||||
"repo_path": "bios/Arcade/Arcade/tourvis.zip",
|
||||
"sha1": "1382217486f524266a1f598d40e6e808209be5a8",
|
||||
"sha256": "b6cbcc04b5ff078c4ca22e4bcc1a012f1ff4a482c96d06964078b33580e9524a",
|
||||
"size": 69276,
|
||||
"repo_path": "bios/Arcade/MAME/tourvis.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
},
|
||||
{
|
||||
"dest": "v4bios.zip",
|
||||
"sha1": "5024b3bfd04ccd2061eb60d2eca254e8faf44f5c",
|
||||
"sha256": "29e05af157d01d146c7ec501e99b7931d78920beccf9b66c9b310c860941e37f",
|
||||
"size": 3753,
|
||||
"repo_path": "bios/Arcade/Arcade/v4bios.zip",
|
||||
"sha1": "9964f68aadd3eef4daa4c0d946422821b4352208",
|
||||
"sha256": "cf23bfd8a445d0888afbc198ca5edce049a70ce9f5f02a1561ee4bc29efdd1d9",
|
||||
"size": 3775,
|
||||
"repo_path": "bios/Arcade/MAME/v4bios.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
@@ -22635,10 +22635,10 @@
|
||||
},
|
||||
{
|
||||
"dest": "pumpitup.zip",
|
||||
"sha1": "6c9139fe4e094088029e545ab62520052971906a",
|
||||
"sha256": "9f86200a6e51e12bbb4526f1bd8c05fcd22112a21c04566582f732da8f750835",
|
||||
"size": 1838700,
|
||||
"repo_path": "bios/Arcade/Arcade/pumpitup.zip",
|
||||
"sha1": "a32161e3b5904f3eb87a156d35fbd941c8f913d0",
|
||||
"sha256": "4a9187a43ff9ccae171ecfed8284af57328d7ac645516bdb763b227b54b6ddc1",
|
||||
"size": 7985810,
|
||||
"repo_path": "bios/Arcade/MAME/pumpitup.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
@@ -22795,10 +22795,10 @@
|
||||
},
|
||||
{
|
||||
"dest": "adam_spi.zip",
|
||||
"sha1": "e7328f43ceea8c9c0a6dc814680da9ff46e93fd3",
|
||||
"sha256": "3ece02358b7292d5525d5b648412e7028cfa7cf670c6351c955bfc19351ce58c",
|
||||
"size": 446843,
|
||||
"repo_path": "bios/Arcade/Arcade/adam_spi.zip",
|
||||
"sha1": "f534c2cfc9729f06b13f87ce62fbd8f63731e160",
|
||||
"sha256": "a52e597afbb2dd19fd31a9bc51199d8811f4369ab8ab9a30cfb5c9a9c24309c2",
|
||||
"size": 576494,
|
||||
"repo_path": "bios/Arcade/MAME/adam_spi.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
@@ -23235,10 +23235,10 @@
|
||||
},
|
||||
{
|
||||
"dest": "qsound.zip",
|
||||
"sha1": "f1ccedb2c6e8f15cfebab52c3923e4b35f4df43f",
|
||||
"sha256": "de8cfe2c1a0207b02a937a1cde4c56040e6d5aa7d0f52c26652a35d9f07dd9b6",
|
||||
"size": 2654,
|
||||
"repo_path": "bios/Arcade/Arcade/qsound.zip",
|
||||
"sha1": "b875be47c087d7a0d484d29232a23fd0bae736e6",
|
||||
"sha256": "55feb9b70dacb7364f0310ed16cc9281b5b0a31318b9e2e0587ae06d15f95356",
|
||||
"size": 2676,
|
||||
"repo_path": "bios/Arcade/MAME/qsound.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
@@ -23255,10 +23255,10 @@
|
||||
},
|
||||
{
|
||||
"dest": "ym2608.zip",
|
||||
"sha1": "06fc753d015b43ca1787f4cfd9331b1674202e64",
|
||||
"sha256": "187ede0f57d1439092a7ac50ea923533871ab7ee65e29baabc4dcdfa7ad4823f",
|
||||
"size": 7609,
|
||||
"repo_path": "bios/Arcade/Arcade/ym2608.zip",
|
||||
"sha1": "1214e3207e4dc85cc1597c53abd70802049dda2c",
|
||||
"sha256": "817aabbfe1ed8734a6cbb88d72178a80b6074dea155cfe1c95f2e4eb8990d1be",
|
||||
"size": 7587,
|
||||
"repo_path": "bios/Arcade/MAME/ym2608.zip",
|
||||
"cores": [
|
||||
"mame"
|
||||
]
|
||||
@@ -28063,6 +28063,16 @@
|
||||
"px68k"
|
||||
]
|
||||
},
|
||||
{
|
||||
"dest": "quasi88/n88sub.rom",
|
||||
"sha1": "bb7103a0818850a039c67ff666a31ce49a8d516f",
|
||||
"sha256": "9dc63118728a171ce2ed437ad38ef280b8b7bcf4a5a95fedf18946501b010456",
|
||||
"size": 2048,
|
||||
"repo_path": "bios/NEC/PC-98/N88SUB.ROM",
|
||||
"cores": [
|
||||
"quasi88"
|
||||
]
|
||||
},
|
||||
{
|
||||
"dest": "quasi88/n88knj2.rom",
|
||||
"sha1": "b86b932cb4ce34472e0fe0f01a4a4350e15dcb53",
|
||||
@@ -29905,10 +29915,10 @@
|
||||
},
|
||||
{
|
||||
"dest": "quasi88/font.rom",
|
||||
"sha1": "78ba9960f135372825ab7244b5e4e73a810002ff",
|
||||
"sha256": "8db165d567e13bce3f50ee495975671faa5a0ff332dcb08e187aa91125c81ec6",
|
||||
"size": 288768,
|
||||
"repo_path": "bios/NEC/PC-98/font.rom",
|
||||
"sha1": "84b90f69671d4b72e8f219e1fe7cd667e976cf7f",
|
||||
"sha256": "3227087599afd9304f260cdeb3bf1533e39e425cf444ceb3c24f55f561e71a0c",
|
||||
"size": 2048,
|
||||
"repo_path": "bios/NEC/PC-88/font.rom",
|
||||
"cores": [
|
||||
"quasi88"
|
||||
]
|
||||
@@ -31917,6 +31927,16 @@
|
||||
"pcsx2"
|
||||
]
|
||||
},
|
||||
{
|
||||
"dest": "message.wav",
|
||||
"sha1": "c54f04b63ac6f04e0a0944ef260b5497e4494dbd",
|
||||
"sha256": "dfd8035387a463722ad07af3c91934e0a685f2afa57e31d305f0602656d1edb1",
|
||||
"size": 15696,
|
||||
"repo_path": "bios/Sony/PlayStation 2/resources/sounds/achievements/message.wav",
|
||||
"cores": [
|
||||
"pcsx2"
|
||||
]
|
||||
},
|
||||
{
|
||||
"dest": "rom1.bin",
|
||||
"sha1": "47d2ec4b342649e4c391043ab915d4435f9d180d",
|
||||
|
||||
+505
-1835
File diff suppressed because it is too large.
Load diff
+531
-521
File diff suppressed because it is too large.
Load diff
+1910
-1240
File diff suppressed because it is too large.
Load diff
+1882
-1902
File diff suppressed because it is too large.
Load diff
+1179
-1819
File diff suppressed because it is too large.
Load diff
+7
-7
@@ -5,7 +5,7 @@
|
||||
"platform": "romm",
|
||||
"display_name": "RomM",
|
||||
"version": "1.0",
|
||||
"generated": "2026-10-04T20:41:16Z",
|
||||
"generated": "2026-10-05T20:14:26Z",
|
||||
"base_destination": "bios",
|
||||
"detect": [
|
||||
{
|
||||
@@ -16,9 +16,9 @@
|
||||
],
|
||||
"standalone_copies": [],
|
||||
"total_files": 958,
|
||||
"total_size": 1580741043,
|
||||
"total_size": 1580732707,
|
||||
"pack_files": 960,
|
||||
"pack_size": 1580741043,
|
||||
"pack_size": 1580732707,
|
||||
"total_omitted": 0,
|
||||
"omitted_files": [],
|
||||
"files": [
|
||||
@@ -3324,10 +3324,10 @@
|
||||
},
|
||||
{
|
||||
"dest": "colecovision/fbneo/hiscore.dat",
|
||||
"sha1": "7381472bf046126257e51a0124e4553282f020e5",
|
||||
"sha256": "9d44fc928c0df41debad1b86739bd6ca96a5891b3b499f6fb3d7e9ea95ca8de5",
|
||||
"size": 322458,
|
||||
"repo_path": "bios/Arcade/Arcade/hiscore.dat",
|
||||
"sha1": "ae3329abbcb7b445d979321a1f67733aa3f023d0",
|
||||
"sha256": "d9c51a13ec2c6db6c9ba0f955c8a8fd8fe54fc2ec05cd82226f4ddf53d500182",
|
||||
"size": 314122,
|
||||
"repo_path": "bios/Arcade/FBNeo/hiscore.dat",
|
||||
"cores": [
|
||||
"fbneo"
|
||||
]
|
||||
|
||||
@@ -195,8 +195,11 @@ shared_groups:
|
||||
destination: quasi88/N88EXT3.ROM
|
||||
required: true
|
||||
md5: "fc4b76a402ba501e6ba6de4b3e8b4273"
|
||||
# Alternative naming from libretro docs (lowercase, _N suffix)
|
||||
# ref: quasi88/src/libretro.c — core accepts both conventions
|
||||
# N88EXT0-3.ROM above are the mkII SR extension ROMs that match N88.ROM
|
||||
# (MAME pc8801mk2sr: 710a63ec, c0bd2aa6, af2b6efa, 7713c519). The core
|
||||
# reads N88EXTn.ROM before n88_n.rom (src/LIBRETRO/libretro.c:109-112),
|
||||
# so they win over the n88_n.rom dumps System.dat pairs with that main ROM,
|
||||
# which come from other models (n88_0 61984bab is the original PC-8801's).
|
||||
- name: n88_0.rom
|
||||
destination: quasi88/n88_0.rom
|
||||
required: false
|
||||
|
||||
@@ -1552,7 +1552,7 @@ systems:
|
||||
docs: "https://docs.libretro.com/library/np2kai/"
|
||||
includes:
|
||||
- np2kai
|
||||
nec---pc-8801:
|
||||
nec-pc-88:
|
||||
files:
|
||||
- name: n88_0.rom
|
||||
destination: quasi88/n88_0.rom
|
||||
@@ -1645,6 +1645,11 @@ systems:
|
||||
native_system: NEC - PC-8801
|
||||
native_path: quasi88/n88knj2.rom
|
||||
native_id: NEC - PC-8801
|
||||
core: quasi88
|
||||
manufacturer: NEC
|
||||
docs: "https://docs.libretro.com/library/quasi88/"
|
||||
includes:
|
||||
- quasi88
|
||||
nec-pc-fx:
|
||||
files:
|
||||
- name: fx-scsi.rom
|
||||
@@ -2005,7 +2010,7 @@ systems:
|
||||
core: desmume
|
||||
manufacturer: Nintendo
|
||||
docs: "https://docs.libretro.com/library/desmume/"
|
||||
nintendo---nintendo-dsi:
|
||||
nintendo-dsi:
|
||||
files:
|
||||
- name: dsi_bios7.bin
|
||||
destination: dsi_bios7.bin
|
||||
@@ -2367,7 +2372,7 @@ systems:
|
||||
size: 1024
|
||||
native_system: Phillips - Videopac+
|
||||
native_id: Phillips - Videopac+
|
||||
philips---cd-i:
|
||||
philips-cdi:
|
||||
files:
|
||||
- name: cdimono1.zip
|
||||
destination: cdimono1.zip
|
||||
@@ -4721,12 +4726,6 @@ systems:
|
||||
core: scummvm
|
||||
manufacturer: Various
|
||||
docs: "https://docs.libretro.com/library/scummvm/"
|
||||
nec-pc-88:
|
||||
includes:
|
||||
- quasi88
|
||||
core: quasi88
|
||||
manufacturer: NEC
|
||||
docs: "https://docs.libretro.com/library/quasi88/"
|
||||
xrick:
|
||||
files:
|
||||
- name: data.zip
|
||||
|
||||
+34
-2
@@ -468,6 +468,36 @@ def _resolve_agnostic(file_entry: dict, files_db: dict, has_strong_hash: bool):
|
||||
return None
|
||||
|
||||
|
||||
def _affinity_tokens(text: str) -> set[str]:
|
||||
return {
|
||||
token
|
||||
for token in (re.sub(r"[^0-9a-z]", "", part.casefold()) for part in text.split("/"))
|
||||
if token
|
||||
}
|
||||
|
||||
|
||||
def _by_affinity(paths: list[str], file_entry: dict, dest_hint: str) -> list[str]:
|
||||
"""Same-named candidates, the ones stored with the file's owner first.
|
||||
|
||||
A name step without a tie-break took whichever path sorted first: the
|
||||
TheXTech coin.ogg for Syobon Action, a Hurrican sound for ARMSX2. The
|
||||
destination's directories and the profile that asks name where its own
|
||||
copy lives; the order is otherwise kept.
|
||||
"""
|
||||
hint = dest_hint or file_entry.get("path") or file_entry.get("destination") or ""
|
||||
wanted = _affinity_tokens(hint.rsplit("/", 1)[0] if "/" in hint else "")
|
||||
for owner in (file_entry.get("source_profile"), file_entry.get("source_emulator")):
|
||||
if owner:
|
||||
wanted |= _affinity_tokens(str(owner).replace(" ", ""))
|
||||
if not wanted or len(paths) < 2:
|
||||
return paths
|
||||
|
||||
def score(path: str) -> int:
|
||||
return len(wanted & _affinity_tokens(path.rsplit("/", 1)[0]))
|
||||
|
||||
return sorted(paths, key=score, reverse=True)
|
||||
|
||||
|
||||
def resolve_local_file(
|
||||
file_entry: dict,
|
||||
db: dict,
|
||||
@@ -737,6 +767,7 @@ def resolve_local_file(
|
||||
if candidates:
|
||||
if zipped_file:
|
||||
candidates = [p for p in candidates if ".zip" in os.path.basename(p)]
|
||||
candidates = _by_affinity(candidates, file_entry, dest_hint)
|
||||
primary = [p for p in candidates if "/.variants/" not in p]
|
||||
if primary or candidates:
|
||||
return (primary[0] if primary else candidates[0]), "name_exact"
|
||||
@@ -795,8 +826,9 @@ def resolve_local_file(
|
||||
return (primary[0] if primary else valid[0][0]), "hash_mismatch"
|
||||
# No candidate contains the zipped_file -fall through to step 5
|
||||
elif not unsourceable:
|
||||
primary = [c[0] for c in candidates if "/.variants/" not in c[0]]
|
||||
return (primary[0] if primary else candidates[0][0]), "hash_mismatch"
|
||||
ordered = _by_affinity([c[0] for c in candidates], file_entry, dest_hint)
|
||||
primary = [c for c in ordered if "/.variants/" not in c]
|
||||
return (primary[0] if primary else ordered[0]), "hash_mismatch"
|
||||
|
||||
# 5. zipped_file content match via pre-built index (last resort:
|
||||
# matches inner ROM MD5 across ALL ZIPs in the repo, so only use
|
||||
|
||||
+28
-10
@@ -267,16 +267,28 @@ def export_platform(
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(content, encoding="utf-8")
|
||||
|
||||
# Only what the format can state: a correction to a field the file has
|
||||
# no place for is not a change the maintainer will find in the diff, and
|
||||
# counting it would announce work the export did not do.
|
||||
carried = exporter.carries()
|
||||
applied = [
|
||||
correction
|
||||
for correction in report.hashes_corrected
|
||||
if correction.rsplit(" ", 1)[-1] in carried
|
||||
]
|
||||
requirements = len(report.required_corrected) if "required" in carried else 0
|
||||
# Only what the format states: a correction to a field the file has no
|
||||
# place for, or one its render keeps out, is not a change the maintainer
|
||||
# will find in the diff, and counting it would announce work the export
|
||||
# did not do. A hash written where the platform left none changes the
|
||||
# check from existence to content, so it is counted too.
|
||||
applied: list[str] = []
|
||||
filled: list[str] = []
|
||||
requirements = 0
|
||||
for system in systems.values():
|
||||
for entry in system.files:
|
||||
for field_name in entry.corrections:
|
||||
if not exporter.states(entry, field_name):
|
||||
continue
|
||||
if field_name == "required":
|
||||
requirements += 1
|
||||
else:
|
||||
applied.append(f"{entry.native_system}/{entry.name} {field_name}")
|
||||
filled.extend(
|
||||
f"{entry.native_system}/{entry.name} {field_name}"
|
||||
for field_name in entry.filled
|
||||
if exporter.states(entry, field_name)
|
||||
)
|
||||
|
||||
landed = 0
|
||||
refused = 0
|
||||
@@ -300,6 +312,8 @@ def export_platform(
|
||||
f"{report.files_kept} kept, {landed} added, "
|
||||
f"{len(applied)} hashes corrected, {requirements} requirements corrected"
|
||||
)
|
||||
if filled:
|
||||
summary += f", {len(filled)} hashes filled"
|
||||
if refused:
|
||||
summary += f", {refused} the format cannot state"
|
||||
if lost:
|
||||
@@ -309,6 +323,10 @@ def export_platform(
|
||||
messages.append(f"hash corrected: {correction}")
|
||||
if len(applied) > 5:
|
||||
messages.append(f"and {len(applied) - 5} more hash corrections")
|
||||
for fill in filled[:5]:
|
||||
messages.append(f"hash filled: {fill}")
|
||||
if len(filled) > 5:
|
||||
messages.append(f"and {len(filled) - 5} more hash fills")
|
||||
|
||||
messages.extend(f"INVALID: {issue}" for issue in issues[:10])
|
||||
if len(issues) > 10:
|
||||
|
||||
@@ -133,6 +133,15 @@ class BaseExporter(ABC):
|
||||
require = require or cls.requires()
|
||||
return not require or bool(fe.hash(require))
|
||||
|
||||
def states(self, fe: NativeFile, field_name: str) -> bool:
|
||||
"""Whether the written file carries this entry's corrected field.
|
||||
|
||||
The summary counts what lands, read entry by entry: a correction the
|
||||
model makes and the format keeps out of the file is not a change the
|
||||
maintainer will find in the diff.
|
||||
"""
|
||||
return field_name in self.carries()
|
||||
|
||||
def outcome(
|
||||
self,
|
||||
systems: dict[str, NativeSystem],
|
||||
|
||||
@@ -48,6 +48,8 @@ class NativeFile:
|
||||
platform: dict | None = None
|
||||
truth: dict | None = None
|
||||
corrections: list[str] = field(default_factory=list)
|
||||
# Hash fields the platform left empty and the truth fills.
|
||||
filled: list[str] = field(default_factory=list)
|
||||
|
||||
@property
|
||||
def origin(self) -> str:
|
||||
@@ -104,7 +106,16 @@ class NativeFile:
|
||||
return None
|
||||
|
||||
def size(self) -> int | None:
|
||||
for entry in (self.truth, self.platform):
|
||||
"""Size of the content the written hashes describe.
|
||||
|
||||
hashes() keeps the platform's values when the truth has none, so the
|
||||
truth's size is only taken when the truth also speaks for the hash:
|
||||
a name-matched 480-byte fbneo boot.bin turned RomM's Dreamcast
|
||||
boot.bin into size 480 beside its 2 MB md5, which never verifies.
|
||||
"""
|
||||
truth_hashed = any(_hash_values(self.truth or {}, f) for f in HASH_FIELDS)
|
||||
order = (self.truth, self.platform) if truth_hashed else (self.platform, self.truth)
|
||||
for entry in order:
|
||||
if entry and entry.get("size"):
|
||||
return int(entry["size"])
|
||||
return None
|
||||
@@ -310,6 +321,8 @@ def build_native_model(
|
||||
report.hashes_corrected.append(
|
||||
f"{matched.native_system}/{matched.name} {field_name}"
|
||||
)
|
||||
elif ours and not theirs:
|
||||
matched.filled.append(field_name)
|
||||
t_req = truth_entry.get("required")
|
||||
p_req = (matched.platform or {}).get("required")
|
||||
if (
|
||||
@@ -323,6 +336,18 @@ def build_native_model(
|
||||
)
|
||||
continue
|
||||
|
||||
# A file another core's entry already claimed is still the
|
||||
# platform's file, not a new one: scph101.bin, declared by two
|
||||
# PSX cores, was written into RetroDECK's manifest a second time.
|
||||
declared = [
|
||||
candidate
|
||||
for native_id in target_ids
|
||||
for candidate in systems.get(native_id, NativeSystem(native_id)).files
|
||||
if candidate.platform is not None
|
||||
]
|
||||
if any(by_destination(c) or by_name(c) for c in declared):
|
||||
continue
|
||||
|
||||
# The truth knows a file the platform does not declare.
|
||||
native_id = target_ids[0]
|
||||
system = systems.get(native_id)
|
||||
|
||||
@@ -18,7 +18,7 @@ sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
from scraper.emudeck_scraper import FUNCTION_HASH_MAP, _RE_FUNC, _RE_LOCAL_HASHES
|
||||
|
||||
from .base_exporter import BaseExporter
|
||||
from .baseline import NativeSystem, Report
|
||||
from .baseline import NativeFile, NativeSystem, Report, _hash_values
|
||||
|
||||
SOURCE_URL = (
|
||||
"https://raw.githubusercontent.com/dragoonDorise/EmuDeck/main"
|
||||
@@ -71,6 +71,27 @@ class Exporter(BaseExporter):
|
||||
"""
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def _entry_md5s(fe: NativeFile) -> list[str]:
|
||||
"""The values one platform entry puts in the array, corrected in place.
|
||||
|
||||
An entry keeps its own values unless the truth contradicts all of
|
||||
them, and then the truth's replace them. An entry without a hash
|
||||
adds nothing, and the truth's extra accepted revisions are not
|
||||
appended: either would grow an array other consumers read too.
|
||||
"""
|
||||
theirs = _hash_values(fe.platform or {}, "md5")
|
||||
if not theirs:
|
||||
return []
|
||||
if "md5" in fe.corrections:
|
||||
return fe.hashes("md5")
|
||||
return theirs
|
||||
|
||||
def states(self, fe: NativeFile, field_name: str) -> bool:
|
||||
if field_name in fe.filled:
|
||||
return False
|
||||
return super().states(fe, field_name)
|
||||
|
||||
@classmethod
|
||||
def _md5s(cls, systems: dict[str, NativeSystem], system_id: str) -> list[str]:
|
||||
"""Every MD5 the system accepts, in a stable order, deduplicated."""
|
||||
@@ -81,7 +102,7 @@ class Exporter(BaseExporter):
|
||||
for fe in system.files:
|
||||
if not cls.writable(fe):
|
||||
continue
|
||||
for value in fe.hashes("md5"):
|
||||
for value in cls._entry_md5s(fe):
|
||||
if _MD5.match(value) and value not in seen:
|
||||
seen.append(value)
|
||||
return seen
|
||||
|
||||
@@ -10,7 +10,7 @@ import json
|
||||
from collections import OrderedDict
|
||||
|
||||
from .base_exporter import BaseExporter
|
||||
from .baseline import NativeSystem, Report
|
||||
from .baseline import NativeFile, NativeSystem, Report
|
||||
|
||||
SOURCE_URL = (
|
||||
"https://raw.githubusercontent.com/RetroBat-Official/emulatorlauncher/master"
|
||||
@@ -41,6 +41,24 @@ class Exporter(BaseExporter):
|
||||
def native_sources() -> dict[str, str]:
|
||||
return {"batocera-systems.json": SOURCE_URL}
|
||||
|
||||
@staticmethod
|
||||
def _md5(fe: NativeFile) -> str:
|
||||
"""The one md5 the entry is checked against, empty for existence.
|
||||
|
||||
RetroBat writes an empty md5 to check a file by existence. Filling it
|
||||
turns that into a content check on a single value: right when the
|
||||
truth accepts exactly one image, wrong when the core takes several
|
||||
and the field can hold only the first.
|
||||
"""
|
||||
if "md5" in fe.filled and len(fe.hashes("md5")) != 1:
|
||||
return ""
|
||||
return fe.hash("md5")
|
||||
|
||||
def states(self, fe: NativeFile, field_name: str) -> bool:
|
||||
if field_name == "md5" and "md5" in fe.filled:
|
||||
return bool(self._md5(fe))
|
||||
return super().states(fe, field_name)
|
||||
|
||||
def render(
|
||||
self,
|
||||
systems: dict[str, NativeSystem],
|
||||
@@ -71,7 +89,7 @@ class Exporter(BaseExporter):
|
||||
bios_files = []
|
||||
for fe in files:
|
||||
entry: OrderedDict[str, str] = OrderedDict()
|
||||
entry["md5"] = fe.hash("md5")
|
||||
entry["md5"] = self._md5(fe)
|
||||
declared = fe.native("native_path", "")
|
||||
entry["file"] = str(declared) if declared else f"bios/{fe.destination}"
|
||||
bios_files.append(entry)
|
||||
|
||||
@@ -19,6 +19,10 @@ COMPONENTS_REPO = "RetroDECK/components"
|
||||
COMPONENTS_BRANCH = "main"
|
||||
RAW_BASE = f"https://raw.githubusercontent.com/{COMPONENTS_REPO}/{COMPONENTS_BRANCH}"
|
||||
MANIFEST = "component_manifest.json"
|
||||
# Labels that only say yes or no, so a corrected requirement can be written
|
||||
# over them. Every other label is a sentence ("At least one BIOS file
|
||||
# required", "Required for some Japanese games.") and is the platform's own.
|
||||
PLAIN_LABELS = ("Required", "Optional")
|
||||
|
||||
|
||||
class Exporter(BaseExporter):
|
||||
@@ -42,6 +46,42 @@ class Exporter(BaseExporter):
|
||||
# one from BIOS data alone would throw the component away.
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def writable(cls, fe: NativeFile, require: str = "") -> bool:
|
||||
"""An addition lands only in a manifest it can be placed in."""
|
||||
if fe.platform is None and not fe.native("component", ""):
|
||||
return False
|
||||
return super().writable(fe, require)
|
||||
|
||||
def states(self, fe: NativeFile, field_name: str) -> bool:
|
||||
if field_name == "required":
|
||||
label = str(fe.native("required_label", ""))
|
||||
return not label or label in PLAIN_LABELS
|
||||
return super().states(fe, field_name)
|
||||
|
||||
@staticmethod
|
||||
def _place_additions(systems: dict[str, NativeSystem]) -> None:
|
||||
"""Give each addition the component its system already lives in.
|
||||
|
||||
A component is one emulator's manifest, and only the platform's
|
||||
entries name it. A system whose entries all sit in one component
|
||||
takes the addition there; one spread over several, or with none of
|
||||
its own, cannot say which manifest is meant, and the addition is
|
||||
left out and counted as such.
|
||||
"""
|
||||
for system in systems.values():
|
||||
components = {
|
||||
str(fe.native("component", ""))
|
||||
for fe in system.files
|
||||
if fe.platform is not None and fe.native("component", "")
|
||||
}
|
||||
if len(components) != 1:
|
||||
continue
|
||||
component = components.pop()
|
||||
for fe in system.files:
|
||||
if fe.platform is None and fe.truth is not None:
|
||||
fe.truth = {**fe.truth, "component": component}
|
||||
|
||||
@staticmethod
|
||||
def component_url(component: str) -> str:
|
||||
return f"{RAW_BASE}/{component}/{MANIFEST}"
|
||||
@@ -73,11 +113,13 @@ class Exporter(BaseExporter):
|
||||
# RetroDECK words the requirement in prose ("Required", "At least one
|
||||
# BIOS file required"), so the platform's own wording is kept and a
|
||||
# boolean is only rendered when there is none to keep.
|
||||
label = fe.native("required_label", "")
|
||||
if label:
|
||||
entry["required"] = str(label)
|
||||
label = str(fe.native("required_label", ""))
|
||||
if label and label not in PLAIN_LABELS:
|
||||
entry["required"] = label
|
||||
elif fe.required:
|
||||
entry["required"] = "Required"
|
||||
elif label:
|
||||
entry["required"] = "Optional"
|
||||
destination = fe.destination
|
||||
if destination and destination not in (fe.name, f"bios/{fe.name}"):
|
||||
directory = destination.rsplit("/", 1)[0]
|
||||
@@ -116,29 +158,47 @@ class Exporter(BaseExporter):
|
||||
truth has something to say and left alone where it does not, and what
|
||||
the platform does not declare is appended.
|
||||
"""
|
||||
by_name: OrderedDict[str, OrderedDict] = OrderedDict()
|
||||
# Keyed by name AND system: the retroarch manifest declares
|
||||
# ATARIOSB.ROM for atari5200 and atari800 with different md5 lists,
|
||||
# and a name-only key wrote the first of ours over both.
|
||||
by_key: OrderedDict[tuple[str, str], OrderedDict] = OrderedDict()
|
||||
for entry in ours:
|
||||
name = str(entry.get("filename", ""))
|
||||
if name and name not in by_name:
|
||||
by_name[name] = entry
|
||||
key = (str(entry.get("filename", "")), str(entry.get("system", "")))
|
||||
if key[0] and key not in by_key:
|
||||
by_key[key] = entry
|
||||
|
||||
merged: list[OrderedDict] = []
|
||||
corrected: set[str] = set()
|
||||
corrected: set[tuple[str, str]] = set()
|
||||
for entry in existing if isinstance(existing, list) else []:
|
||||
if not isinstance(entry, dict):
|
||||
continue
|
||||
name = str(entry.get("filename", ""))
|
||||
ours_entry = by_name.get(name)
|
||||
if ours_entry is None:
|
||||
declared = entry.get("system")
|
||||
# One entry can serve several systems: neogeo.zip is declared
|
||||
# once for neogeo, fbneo and arcade.
|
||||
systems = (
|
||||
[str(s) for s in declared] if isinstance(declared, list)
|
||||
else [str(declared)] if declared else []
|
||||
)
|
||||
keys = [(name, s) for s in systems if (name, s) in by_key]
|
||||
if not systems:
|
||||
# An entry without a system matches ours only when one
|
||||
# system alone declares the name.
|
||||
named = [k for k in by_key if k[0] == name]
|
||||
keys = named if len(named) == 1 else []
|
||||
if not keys:
|
||||
merged.append(OrderedDict(entry))
|
||||
continue
|
||||
combined = OrderedDict(entry)
|
||||
combined.update(ours_entry)
|
||||
combined.update(
|
||||
(field, value) for field, value in by_key[keys[0]].items()
|
||||
if not (field == "system" and declared)
|
||||
)
|
||||
merged.append(combined)
|
||||
corrected.add(name)
|
||||
corrected.update(keys)
|
||||
|
||||
merged.extend(
|
||||
entry for name, entry in by_name.items() if name not in corrected
|
||||
entry for key, entry in by_key.items() if key not in corrected
|
||||
)
|
||||
return merged
|
||||
|
||||
@@ -149,6 +209,7 @@ class Exporter(BaseExporter):
|
||||
originals: dict[str, str],
|
||||
scraped: dict | None = None,
|
||||
) -> dict[str, str]:
|
||||
self._place_additions(systems)
|
||||
grouped = self._by_component(systems)
|
||||
produced: dict[str, str] = {}
|
||||
|
||||
|
||||
@@ -402,9 +402,11 @@ def _collect_all_aliases(files: dict) -> dict:
|
||||
"""
|
||||
md5_to_sha1 = {}
|
||||
name_to_sha1 = {}
|
||||
name_count: dict[str, int] = {}
|
||||
for sha1, entry in files.items():
|
||||
md5_to_sha1[entry["md5"]] = sha1
|
||||
name_to_sha1[entry["name"]] = sha1
|
||||
name_count[entry["name"]] = name_count.get(entry["name"], 0) + 1
|
||||
|
||||
aliases = {}
|
||||
|
||||
@@ -501,7 +503,10 @@ def _collect_all_aliases(files: dict) -> dict:
|
||||
matched = sha1
|
||||
elif md5 and md5 in md5_to_sha1:
|
||||
matched = md5_to_sha1[md5]
|
||||
elif entry_name and entry_name in name_to_sha1:
|
||||
elif entry_name and name_count.get(entry_name) == 1:
|
||||
# A name carried by several files names none of them:
|
||||
# quasi88's disk.rom aliases went to whichever
|
||||
# disk.rom the scan met last, a Tandy CoCo ROM.
|
||||
matched = name_to_sha1[entry_name]
|
||||
if matched:
|
||||
for alias_name in entry_aliases:
|
||||
|
||||
+222
-171
@@ -76,6 +76,7 @@ from validation import (
|
||||
_build_validation_index,
|
||||
check_file_validation,
|
||||
filter_files_by_mode,
|
||||
find_validated_variant,
|
||||
)
|
||||
|
||||
yaml = require_yaml()
|
||||
@@ -130,54 +131,6 @@ def _write_generated_member(zf: zipfile.ZipFile, arcname: str, text: str) -> Non
|
||||
zf.writestr(info, text)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def _pack_member_groups(
|
||||
config: dict,
|
||||
pack_systems: dict,
|
||||
emulators_dir: str,
|
||||
db: dict,
|
||||
base_dest: str,
|
||||
emu_profiles: dict | None,
|
||||
target_cores: set[str] | None,
|
||||
source: str,
|
||||
) -> dict[str, list[tuple[str, str]]]:
|
||||
"""Group every candidate of a pack by system, as (destination, name).
|
||||
|
||||
Covers the platform baseline and the core extras, because a narrowing pass
|
||||
that saw only one of the two would compare an incomplete set.
|
||||
"""
|
||||
groups: dict[str, list[tuple[str, str]]] = {}
|
||||
for sys_id, system in pack_systems.items():
|
||||
members = groups.setdefault(sys_id, [])
|
||||
for fe in system.get("files", []):
|
||||
dest = sanitize_pack_path(fe.get("destination", fe.get("name", "")))
|
||||
if dest:
|
||||
members.append((dest, fe.get("name", "")))
|
||||
if source == "platform":
|
||||
return groups
|
||||
# find_undeclared_files reports the profile's display name, not its key,
|
||||
# so index both: on a key-only lookup nearly every core extra fell into a
|
||||
# single bucket and lost its per-system grouping.
|
||||
emu_systems = _emulator_systems_index(emu_profiles)
|
||||
for fe in _collect_emulator_extras(
|
||||
config, emulators_dir, db, set(), base_dest, emu_profiles,
|
||||
target_cores=target_cores, include_all=(source == "truth"),
|
||||
):
|
||||
dest = sanitize_pack_path(fe.get("destination", fe.get("name", "")))
|
||||
if not dest:
|
||||
continue
|
||||
for sys_id in emu_systems.get(fe.get("source_emulator", ""), ["_extras"]):
|
||||
groups.setdefault(sys_id, []).append((dest, fe.get("name", "")))
|
||||
return groups
|
||||
|
||||
|
||||
def _narrowings(
|
||||
source: str,
|
||||
regions: list[str] | None,
|
||||
@@ -229,6 +182,41 @@ def _target_tag(target_name: str) -> str:
|
||||
)
|
||||
|
||||
|
||||
def _system_tag(system_filter: list[str] | None) -> str:
|
||||
"""Filename tag for a pack limited to some systems, empty for all."""
|
||||
if not system_filter:
|
||||
return ""
|
||||
display_parts = []
|
||||
for sid in system_filter:
|
||||
s = sid.lower().replace("_", "-")
|
||||
for prefix in MANUFACTURER_PREFIXES:
|
||||
if s.startswith(prefix):
|
||||
s = s[len(prefix) :]
|
||||
break
|
||||
display_parts.append("_".join(p.title() for p in s.split("-") if p))
|
||||
return "_" + "_".join(display_parts)
|
||||
|
||||
|
||||
def _platform_pack_stem(
|
||||
group_platforms: list[str], representative: str, platforms_dir: str
|
||||
) -> str:
|
||||
"""Platform part of a pack name, version included.
|
||||
|
||||
A group of identical platforms shares one pack named after all of them;
|
||||
the builder and --verify-packs both read the name from here.
|
||||
"""
|
||||
rep_cfg = load_platform_config(representative, platforms_dir)
|
||||
version = rep_cfg.get("version", rep_cfg.get("dat_version", ""))
|
||||
version_tag = f"_{version.replace(' ', '')}" if version else ""
|
||||
if len(group_platforms) <= 1:
|
||||
return rep_cfg.get("platform", representative).replace(" ", "_") + version_tag
|
||||
names = [
|
||||
load_platform_config(p, platforms_dir).get("platform", p)
|
||||
for p in group_platforms
|
||||
]
|
||||
return "_".join(n.replace(" ", "") for n in names) + version_tag
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -479,16 +467,34 @@ def _preferred_entries(
|
||||
]
|
||||
if not constrained:
|
||||
continue
|
||||
best = None
|
||||
for fe in constrained:
|
||||
_lp, _st = resolve_file(
|
||||
resolved = [
|
||||
(fe, *resolve_file(
|
||||
fe,
|
||||
db,
|
||||
bios_dir,
|
||||
zip_contents,
|
||||
data_dir_registry=data_registry,
|
||||
offline=offline,
|
||||
)
|
||||
))
|
||||
for fe in constrained
|
||||
]
|
||||
members = [fe for fe in entries if fe.get("zipped_file")]
|
||||
if members:
|
||||
# One archive declared once per ROM it must hold: the archive
|
||||
# to ship is the one the most declarations accept, not the first
|
||||
# that answers one of them (adam_fdc.zip: a one-member MAME set
|
||||
# answered first, the eight-member set sat beside it).
|
||||
def accepted(path: str | None) -> int:
|
||||
if not path or not path.endswith(".zip"):
|
||||
return -1
|
||||
return sum(_inner_rom_check(fe, path) == "ok" for fe in members)
|
||||
|
||||
fe, path, _st = max(resolved, key=lambda item: accepted(item[1]))
|
||||
if accepted(path) > 0:
|
||||
preferred_entries[full] = id(fe)
|
||||
continue
|
||||
best = None
|
||||
for fe, _lp, _st in resolved:
|
||||
if _lp and _st == "md5_exact":
|
||||
best = fe
|
||||
break
|
||||
@@ -522,8 +528,10 @@ def _select_variants(
|
||||
"""
|
||||
region_drops: set[str] = set()
|
||||
region_fallbacks: list[str] = []
|
||||
if regions:
|
||||
region_index = region_mod.build_region_index(emu_profiles or {})
|
||||
# One grouping for both passes: a file judged in its own system by one
|
||||
# pass must not land in every system of its profile for the other.
|
||||
region_groups: dict[str, list[tuple[str, str]]] = {}
|
||||
if regions or one_per_slot:
|
||||
region_groups, _extra_dests = platform_region_groups(
|
||||
config,
|
||||
pack_systems,
|
||||
@@ -535,6 +543,8 @@ def _select_variants(
|
||||
include_extras=(source != "platform"),
|
||||
include_all=(source == "truth"),
|
||||
)
|
||||
if regions:
|
||||
region_index = region_mod.build_region_index(emu_profiles or {})
|
||||
region_drops = region_mod.resolve_region_drops(
|
||||
region_groups, region_index, regions
|
||||
)
|
||||
@@ -544,13 +554,9 @@ def _select_variants(
|
||||
|
||||
slot_undecidable: list[str] = []
|
||||
if one_per_slot:
|
||||
members_by_system = _pack_member_groups(
|
||||
config, pack_systems, emulators_dir, db, base_dest,
|
||||
emu_profiles, target_cores, source,
|
||||
)
|
||||
slot_groups = {
|
||||
sys_id: [(d, n) for d, n in members if d not in region_drops]
|
||||
for sys_id, members in members_by_system.items()
|
||||
for sys_id, members in region_groups.items()
|
||||
}
|
||||
slot_drops, slot_undecidable = slot_mod.resolve_slot_drops(
|
||||
slot_groups, slot_mod.build_slot_index(emu_profiles or {})
|
||||
@@ -599,27 +605,12 @@ def generate_pack(
|
||||
platform_display = config.get("platform", platform_name)
|
||||
base_dest = config.get("base_destination", "")
|
||||
|
||||
version = config.get("version", config.get("dat_version", ""))
|
||||
version_tag = f"_{version.replace(' ', '')}" if version else ""
|
||||
narrowings = _narrowings(
|
||||
source, regions, target_name, one_per_slot, required_only
|
||||
)
|
||||
narrow_tags = "".join(tag for tag, _label in narrowings)
|
||||
|
||||
sys_tag = ""
|
||||
if system_filter:
|
||||
display_parts = []
|
||||
for sid in system_filter:
|
||||
s = sid.lower().replace("_", "-")
|
||||
for prefix in MANUFACTURER_PREFIXES:
|
||||
if s.startswith(prefix):
|
||||
s = s[len(prefix) :]
|
||||
break
|
||||
parts = s.split("-")
|
||||
display_parts.append("_".join(p.title() for p in parts if p))
|
||||
sys_tag = "_" + "_".join(display_parts)
|
||||
|
||||
zip_name = f"{platform_display.replace(' ', '_')}{version_tag}{narrow_tags}_BIOS_Pack{sys_tag}.zip"
|
||||
stem = _platform_pack_stem([platform_name], platform_name, platforms_dir)
|
||||
zip_name = f"{stem}{narrow_tags}_BIOS_Pack{_system_tag(system_filter)}.zip"
|
||||
zip_path = os.path.join(output_dir, zip_name)
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
@@ -650,30 +641,15 @@ def generate_pack(
|
||||
from common import resolve_platform_cores
|
||||
|
||||
validation_index = {}
|
||||
slot_overrides: dict[str, str] = {}
|
||||
if emu_profiles:
|
||||
platform_profiles = {
|
||||
name: emu_profiles[name]
|
||||
for name in resolve_platform_cores(config, emu_profiles)
|
||||
}
|
||||
validation_index = _build_validation_index(platform_profiles)
|
||||
# Where a source-verified profile contradicts the scraped baseline on
|
||||
# one destination, the pack answers to the platform it is built for.
|
||||
# In existence mode the frontend never reads the bytes, so serving the
|
||||
# emulator's file satisfies both sides and nothing is traded away.
|
||||
mode = config.get("verification_mode", "existence")
|
||||
for conflict in slots.find_conflicts(
|
||||
config,
|
||||
platform_profiles,
|
||||
db,
|
||||
base_dest,
|
||||
{str(c) for c in config.get("standalone_cores", [])},
|
||||
zip_contents,
|
||||
data_registry,
|
||||
):
|
||||
decision = slots.arbitrate(conflict, mode)
|
||||
if decision.serves_both and decision.winner.local_path:
|
||||
slot_overrides[conflict.destination] = decision.winner.local_path
|
||||
slot_overrides = slots.pack_overrides(
|
||||
config, emu_profiles or {}, db, zip_contents, data_registry
|
||||
)
|
||||
|
||||
# Filter systems by target if specified
|
||||
plat_cores = (
|
||||
@@ -960,12 +936,13 @@ def generate_pack(
|
||||
)
|
||||
if check:
|
||||
reason, emus_list = check
|
||||
better = _find_candidate_satisfying_both(
|
||||
better = find_validated_variant(
|
||||
file_entry,
|
||||
db,
|
||||
local_path,
|
||||
validation_index,
|
||||
bios_dir,
|
||||
platform_digest=digest_algorithm(verification_mode),
|
||||
)
|
||||
if better:
|
||||
local_path = better
|
||||
@@ -1270,6 +1247,7 @@ def generate_emulator_pack(
|
||||
|
||||
# ZIP naming
|
||||
display_names = [p.get("emulator", n).replace(" ", "") for n, p in selected]
|
||||
validation_index = _build_validation_index(dict(selected))
|
||||
narrow_tags = "".join(
|
||||
tag
|
||||
for tag, _label in _narrowings(
|
||||
@@ -1280,25 +1258,9 @@ def generate_emulator_pack(
|
||||
zip_path = os.path.join(output_dir, zip_name)
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
# One group per system (or explicit variant_group) inside each profile.
|
||||
# Multi-system cores such as O2EM therefore retain a fallback BIOS for
|
||||
# every system instead of letting one matching system empty another.
|
||||
region_drops: set[str] = set()
|
||||
if regions:
|
||||
region_index = region_mod.build_region_index(dict(selected))
|
||||
region_groups: dict[str, list[tuple[str, str]]] = {}
|
||||
for emu_name, profile in sorted(selected):
|
||||
structure = profile.get("pack_structure")
|
||||
for fe in filter_files_by_mode(profile.get("files", []), standalone):
|
||||
dest = _resolve_destination(fe, structure, standalone)
|
||||
if dest:
|
||||
group_id = _emulator_region_group(emu_name, profile, fe)
|
||||
region_groups.setdefault(group_id, []).append(
|
||||
(dest, fe.get("name", ""))
|
||||
)
|
||||
region_drops = region_mod.resolve_region_drops(
|
||||
region_groups, region_index, regions
|
||||
)
|
||||
region_drops: set[str] = (
|
||||
emulator_region_drops(selected, standalone, regions) if regions else set()
|
||||
)
|
||||
|
||||
total_files = 0
|
||||
missing_files = []
|
||||
@@ -1447,7 +1409,7 @@ def generate_emulator_pack(
|
||||
|
||||
dest_hint = fe.get("path", "")
|
||||
local_path, status = resolve_file(
|
||||
fe,
|
||||
{**fe, "source_profile": emu_name},
|
||||
db,
|
||||
bios_dir,
|
||||
zip_contents,
|
||||
@@ -1480,6 +1442,18 @@ def generate_emulator_pack(
|
||||
missing_files.append(fe["name"])
|
||||
continue
|
||||
|
||||
# The file verify --emulator credits is the one shipped: a
|
||||
# dump the core's own check rejects gives way to a held one
|
||||
# it accepts (azahar's otp.bin, dolphin's dsp_rom.bin).
|
||||
if check_file_validation(
|
||||
local_path, fe["name"], validation_index, bios_dir
|
||||
):
|
||||
better = find_validated_variant(
|
||||
fe, db, local_path, validation_index, bios_dir
|
||||
)
|
||||
if better:
|
||||
local_path = better
|
||||
|
||||
# SHA1 dedup: skip if same physical file AND same destination
|
||||
# (but allow same file to be packed under different destinations,
|
||||
# e.g., IPL.bin in GC/USA/ and GC/EUR/ from same source)
|
||||
@@ -2054,7 +2028,7 @@ def _validate_args(args, parser):
|
||||
):
|
||||
if given:
|
||||
parser.error(f"{flag} is incompatible with --from-md5")
|
||||
if has_system and has_platform:
|
||||
if has_system and (has_platform or has_all):
|
||||
if args.manifest:
|
||||
parser.error("--system is incompatible with --manifest")
|
||||
if args.split:
|
||||
@@ -2199,10 +2173,13 @@ def _run_verify_packs(args):
|
||||
with open(args.db) as f:
|
||||
verify_db = json.load(f)
|
||||
|
||||
platforms = list_registered_platforms(args.platforms_dir)
|
||||
if args.platform:
|
||||
platforms = [args.platform]
|
||||
elif not args.all:
|
||||
elif args.all:
|
||||
platforms = list_registered_platforms(
|
||||
args.platforms_dir, include_archived=args.include_archived
|
||||
)
|
||||
else:
|
||||
print("ERROR: --verify-packs requires --platform or --all")
|
||||
sys.exit(1)
|
||||
|
||||
@@ -2210,18 +2187,16 @@ def _run_verify_packs(args):
|
||||
verify_regions = getattr(args, "regions", None)
|
||||
verify_profiles = load_emulator_profiles(args.emulators_dir)
|
||||
verify_data_registry = load_data_dir_registry(args.platforms_dir)
|
||||
expected = _expected_pack_names(
|
||||
platforms, args.platforms_dir, verify_regions, args.target
|
||||
)
|
||||
on_disk = (
|
||||
set(os.listdir(args.output_dir)) if os.path.isdir(args.output_dir) else set()
|
||||
)
|
||||
|
||||
for platform_name in platforms:
|
||||
config = load_platform_config(platform_name, args.platforms_dir)
|
||||
display = config.get("platform", platform_name).replace(" ", "_")
|
||||
|
||||
zip_path = None
|
||||
if os.path.isdir(args.output_dir):
|
||||
for entry in sorted(os.listdir(args.output_dir)):
|
||||
if entry.endswith("_BIOS_Pack.zip") and display in entry:
|
||||
zip_path = os.path.join(args.output_dir, entry)
|
||||
break
|
||||
if not zip_path:
|
||||
found = sorted(expected[platform_name] & on_disk)
|
||||
if not found:
|
||||
print(f" {platform_name}: SKIP (no pack in {args.output_dir})")
|
||||
# Naming a platform is asking about its pack. Answering SKIP and
|
||||
# exiting 0 says the pack passed; with --all, a platform whose pack
|
||||
@@ -2229,38 +2204,36 @@ def _run_verify_packs(args):
|
||||
if args.platform:
|
||||
all_ok = False
|
||||
continue
|
||||
|
||||
if _narrows_contents(os.path.basename(zip_path)):
|
||||
print(f" {platform_name}: SKIP (narrowed variant)")
|
||||
continue
|
||||
|
||||
result = verify_pack_against_platform(
|
||||
zip_path,
|
||||
platform_name,
|
||||
args.platforms_dir,
|
||||
db=verify_db,
|
||||
emulators_dir=args.emulators_dir,
|
||||
emu_profiles=verify_profiles,
|
||||
regions=verify_regions,
|
||||
data_registry=verify_data_registry,
|
||||
target_cores=_target_cores_for(
|
||||
platform_name, args.target, args.platforms_dir
|
||||
),
|
||||
)
|
||||
ok, errors = result[0], result[3]
|
||||
bl_checked, bl_present = result[4], result[5]
|
||||
core_checked, core_present = result[6], result[7]
|
||||
counts = (
|
||||
f"{bl_present}/{bl_checked} baseline, "
|
||||
f"{core_present}/{core_checked} cores"
|
||||
)
|
||||
if ok:
|
||||
print(f" {platform_name}: OK ({counts})")
|
||||
else:
|
||||
print(f" {platform_name}: FAIL ({counts}, {len(errors)} errors)")
|
||||
for err in errors[:5]:
|
||||
print(f" {err}")
|
||||
all_ok = False
|
||||
tc = _target_cores_for(platform_name, args.target, args.platforms_dir)
|
||||
# A group pack and a pack built for the platform alone can both be
|
||||
# present; each one is a pack of this platform.
|
||||
for pack_name in found:
|
||||
zip_path = os.path.join(args.output_dir, pack_name)
|
||||
result = verify_pack_against_platform(
|
||||
zip_path,
|
||||
platform_name,
|
||||
args.platforms_dir,
|
||||
db=verify_db,
|
||||
emulators_dir=args.emulators_dir,
|
||||
emu_profiles=verify_profiles,
|
||||
regions=verify_regions,
|
||||
data_registry=verify_data_registry,
|
||||
target_cores=tc,
|
||||
)
|
||||
ok, errors = result[0], result[3]
|
||||
bl_checked, bl_present = result[4], result[5]
|
||||
core_checked, core_present = result[6], result[7]
|
||||
counts = (
|
||||
f"{bl_present}/{bl_checked} baseline, "
|
||||
f"{core_present}/{core_checked} cores"
|
||||
)
|
||||
if ok:
|
||||
print(f" {platform_name} [{pack_name}]: OK ({counts})")
|
||||
else:
|
||||
print(f" {platform_name} [{pack_name}]: FAIL ({counts}, {len(errors)} errors)")
|
||||
for err in errors[:5]:
|
||||
print(f" {err}")
|
||||
all_ok = False
|
||||
|
||||
if not all_ok:
|
||||
sys.exit(1)
|
||||
@@ -2344,13 +2317,6 @@ def _run_platform_packs(
|
||||
offline=args.offline,
|
||||
)
|
||||
if not args.split and zip_path and aliases:
|
||||
rep_cfg = load_platform_config(representative, args.platforms_dir)
|
||||
ver = rep_cfg.get("version", rep_cfg.get("dat_version", ""))
|
||||
ver_tag = f"_{ver.replace(' ', '')}" if ver else ""
|
||||
all_names = [
|
||||
load_platform_config(p, args.platforms_dir).get("platform", p)
|
||||
for p in group_platforms
|
||||
]
|
||||
narrow_tags = "".join(
|
||||
tag
|
||||
for tag, _label in _narrowings(
|
||||
@@ -2361,9 +2327,12 @@ def _run_platform_packs(
|
||||
required_only,
|
||||
)
|
||||
)
|
||||
stem = _platform_pack_stem(
|
||||
group_platforms, representative, args.platforms_dir
|
||||
)
|
||||
combined = (
|
||||
"_".join(n.replace(" ", "") for n in all_names)
|
||||
+ f"{ver_tag}{narrow_tags}_BIOS_Pack.zip"
|
||||
f"{stem}{narrow_tags}_BIOS_Pack"
|
||||
f"{_system_tag(system_filter)}.zip"
|
||||
)
|
||||
new_path = os.path.join(os.path.dirname(zip_path), combined)
|
||||
if new_path != zip_path:
|
||||
@@ -2516,6 +2485,32 @@ def main():
|
||||
if args.one_per_slot and args.manifest_targets:
|
||||
parser.error("--one-per-slot is incompatible with --manifest-targets")
|
||||
|
||||
# --all-variants builds the six source x required combinations itself:
|
||||
# a --source or --required-only beside it would be overridden, and the
|
||||
# modes that build one pack (emulator, system, hashes) never read it.
|
||||
if args.all_variants:
|
||||
for flag, given in (
|
||||
("--source", args.source != "full"),
|
||||
("--required-only", args.required_only),
|
||||
("--emulator", args.emulator),
|
||||
("--from-md5", args.from_md5 or args.from_md5_file),
|
||||
):
|
||||
if given:
|
||||
parser.error(f"{flag} is incompatible with --all-variants")
|
||||
if args.system and not (args.platform or args.all):
|
||||
parser.error("--all-variants requires --platform or --all")
|
||||
if args.verify_packs and args.manifest:
|
||||
parser.error("--verify-packs is incompatible with --manifest")
|
||||
if args.verify_packs:
|
||||
# The check that follows the build reads the full pack's name.
|
||||
for flag, given in (
|
||||
("--system", args.system),
|
||||
("--one-per-slot", args.one_per_slot),
|
||||
("--split", args.split),
|
||||
):
|
||||
if given:
|
||||
parser.error(f"{flag} is incompatible with --verify-packs")
|
||||
|
||||
# Quick-exit modes: --verify-packs alone = verify existing packs only
|
||||
# Combined with --all-variants, generation runs first then verify
|
||||
if args.verify_packs and not args.all_variants:
|
||||
@@ -2527,6 +2522,7 @@ def main():
|
||||
("--one-per-slot", args.one_per_slot),
|
||||
("--required-only", args.required_only),
|
||||
("--source", args.source != "full"),
|
||||
("--system", args.system),
|
||||
):
|
||||
if given:
|
||||
parser.error(f"{flag} is incompatible with --verify-packs")
|
||||
@@ -2729,6 +2725,9 @@ def main():
|
||||
target_cores_cache,
|
||||
system_filter,
|
||||
)
|
||||
if args.verify_packs:
|
||||
with _pack_output_lock(args.output_dir, exclusive=False):
|
||||
_run_verify_packs(args)
|
||||
|
||||
|
||||
# Manifest generation (JSON inventory for install.py)
|
||||
@@ -2968,6 +2967,9 @@ def generate_manifest(
|
||||
pack_only_sizes: list[int] = []
|
||||
if data_registry is None:
|
||||
data_registry = load_data_dir_registry(platforms_dir)
|
||||
slot_overrides = slots.pack_overrides(
|
||||
config, emu_profiles, db, zip_contents, data_registry
|
||||
)
|
||||
|
||||
def manifest_destination(full_destination: str) -> str:
|
||||
if base_dest and full_destination.startswith(f"{base_dest}/"):
|
||||
@@ -3063,6 +3065,9 @@ def generate_manifest(
|
||||
data_dir_registry=data_registry,
|
||||
offline=offline,
|
||||
)
|
||||
override = slot_overrides.get(full_dest)
|
||||
if override:
|
||||
local_path, status = override, "slot_arbitrated"
|
||||
if (
|
||||
status == "hash_mismatch"
|
||||
and local_path
|
||||
@@ -3261,6 +3266,52 @@ def _target_cores_for(
|
||||
return cache.get(platform_name)
|
||||
|
||||
|
||||
def _expected_pack_names(
|
||||
platforms: list[str],
|
||||
platforms_dir: str,
|
||||
regions: list[str] | None,
|
||||
target_name: str | None,
|
||||
) -> dict[str, set[str]]:
|
||||
"""Exact pack file names --verify-packs may check, per platform.
|
||||
|
||||
Rebuilt from the name builder, never matched by substring: a substring
|
||||
picks the full pack when a regional or targeted one was asked for, or a
|
||||
narrowed pack when the full one was. A platform answers to its own name
|
||||
and to the name of the group it shares a pack with.
|
||||
"""
|
||||
narrow_tags = "".join(
|
||||
tag for tag, _label in _narrowings("full", regions, target_name, False, False)
|
||||
)
|
||||
names: dict[str, set[str]] = {
|
||||
p: {f"{_platform_pack_stem([p], p, platforms_dir)}{narrow_tags}_BIOS_Pack.zip"}
|
||||
for p in platforms
|
||||
}
|
||||
# Groups depend on which platforms a run built: --all alone or with
|
||||
# --include-archived. Both compositions are names a pack can carry.
|
||||
for include_archived in (False, True):
|
||||
pool = list_registered_platforms(
|
||||
platforms_dir, include_archived=include_archived
|
||||
)
|
||||
cache = None
|
||||
if target_name:
|
||||
cache, pool = build_target_cores_cache(
|
||||
pool, target_name, platforms_dir, is_all=True
|
||||
)
|
||||
for group, representative in group_identical_platforms(
|
||||
pool, platforms_dir, cache
|
||||
):
|
||||
if len(group) < 2:
|
||||
continue
|
||||
name = (
|
||||
f"{_platform_pack_stem(group, representative, platforms_dir)}"
|
||||
f"{narrow_tags}_BIOS_Pack.zip"
|
||||
)
|
||||
for p in group:
|
||||
if p in names:
|
||||
names[p].add(name)
|
||||
return names
|
||||
|
||||
|
||||
def _content_narrowing_tags() -> tuple[str, ...]:
|
||||
"""Tags naming a pack that holds fewer files than the platform declares.
|
||||
|
||||
@@ -3339,7 +3390,7 @@ def verify_and_finalize_packs(
|
||||
# Map ZIP names to platform names
|
||||
pack_to_platform: dict[str, list[str]] = {}
|
||||
for name in _pack_archives(output_dir):
|
||||
for pname in list_registered_platforms(platforms_dir):
|
||||
for pname in list_registered_platforms(platforms_dir, include_archived=True):
|
||||
cfg = load_platform_config(pname, platforms_dir)
|
||||
display = cfg.get("platform", pname).replace(" ", "_")
|
||||
if display in name or display.replace("_", "") in name.replace("_", ""):
|
||||
@@ -3421,6 +3472,7 @@ from packextras import ( # noqa: E402,F401
|
||||
_collect_emulator_extras,
|
||||
_extra_system_ids,
|
||||
platform_region_groups,
|
||||
emulator_region_drops,
|
||||
_emulator_region_group,
|
||||
)
|
||||
|
||||
@@ -3447,7 +3499,6 @@ from packresolve import ( # noqa: E402,F401
|
||||
parse_hash_input,
|
||||
parse_hash_file,
|
||||
lookup_hashes,
|
||||
_find_candidate_satisfying_both,
|
||||
resolve_file,
|
||||
)
|
||||
|
||||
|
||||
+74
-18
@@ -139,6 +139,7 @@ def _agnostic_scan_extras(
|
||||
by_name: dict,
|
||||
seen_dests: set,
|
||||
extras_prefix: str,
|
||||
standalone_set: set[str] | None = None,
|
||||
) -> list[dict]:
|
||||
"""Every interchangeable candidate a filename-agnostic core accepts.
|
||||
|
||||
@@ -203,6 +204,14 @@ def _agnostic_scan_extras(
|
||||
continue
|
||||
if not (f.get("size") or f.get("min_size") or f.get("max_size")):
|
||||
continue
|
||||
# The free name is the BIOS directory's: a shader, a sound or a
|
||||
# font the emulator reads from its resources tree has a fixed
|
||||
# name and must not seed a scan (yaps2 flattened 189 of them to
|
||||
# the root of the RetroArch pack).
|
||||
bios_dir = profile.get("bios_directory")
|
||||
fpath = f.get("path") or ""
|
||||
if bios_dir and "/" in fpath and not fpath.startswith(bios_dir):
|
||||
continue
|
||||
|
||||
path_prefix = resolved_dirs.get(id(f), "")
|
||||
if not path_prefix:
|
||||
@@ -224,7 +233,13 @@ def _agnostic_scan_extras(
|
||||
scan_name = entry.get("name", "")
|
||||
if not scan_name:
|
||||
continue
|
||||
dest = scan_name
|
||||
# Into the directory the seed entry names, not the root:
|
||||
# yaps2 reads its BIOS from pcsx2/bios/.
|
||||
seed = f.get("path") or ""
|
||||
if runs_standalone(emu_name, profile, standalone_set or set()):
|
||||
seed = f.get("standalone_path") or seed
|
||||
folder = seed.rsplit("/", 1)[0] if "/" in seed else ""
|
||||
dest = f"{folder}/{scan_name}" if folder else scan_name
|
||||
full_dest = f"{extras_prefix}/{dest}" if extras_prefix else dest
|
||||
if full_dest in seen_dests:
|
||||
continue
|
||||
@@ -497,20 +512,25 @@ def _collect_emulator_extras(
|
||||
):
|
||||
continue
|
||||
seen_dests.add(full_dest)
|
||||
extras.append(
|
||||
{
|
||||
"name": fname,
|
||||
"destination": dest,
|
||||
"required": f.get("required", False),
|
||||
"hle_fallback": f.get("hle_fallback", False),
|
||||
"source_emulator": profile.get("emulator", emu_name),
|
||||
"source_profile": emu_name,
|
||||
"source_system": f.get("system"),
|
||||
"source_systems": list(profile.get("systems", [])),
|
||||
"region": f.get("region"),
|
||||
"variant_group": f.get("variant_group"),
|
||||
}
|
||||
)
|
||||
extra = {
|
||||
"name": fname,
|
||||
"destination": dest,
|
||||
"required": f.get("required", False),
|
||||
"hle_fallback": f.get("hle_fallback", False),
|
||||
"source_emulator": profile.get("emulator", emu_name),
|
||||
"source_profile": emu_name,
|
||||
"source_system": f.get("system"),
|
||||
"source_systems": list(profile.get("systems", [])),
|
||||
"region": f.get("region"),
|
||||
"variant_group": f.get("variant_group"),
|
||||
}
|
||||
# The same identity the first pass carries: without it the copy
|
||||
# resolves on its name alone and a same-named file of another
|
||||
# game fills the slot the profile marks as not held.
|
||||
for identity in ("sha1", "md5", "sha256", "crc32", "size", "unsourceable"):
|
||||
if f.get(identity) not in (None, "", []):
|
||||
extra[identity] = f[identity]
|
||||
extras.append(extra)
|
||||
|
||||
extras.extend(
|
||||
_archive_prefix_extras(
|
||||
@@ -521,7 +541,8 @@ def _collect_emulator_extras(
|
||||
|
||||
extras.extend(
|
||||
_agnostic_scan_extras(
|
||||
profiles, relevant, db, by_name, seen_dests, extras_prefix
|
||||
profiles, relevant, db, by_name, seen_dests, extras_prefix,
|
||||
standalone_set,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -590,11 +611,46 @@ def platform_region_groups(
|
||||
)
|
||||
] = dest
|
||||
variant = extra.get("variant_group")
|
||||
for sys_id in _extra_system_ids(extra) or ["_extras"]:
|
||||
group_id = f"{sys_id}:variant:{variant}" if variant else sys_id
|
||||
if variant:
|
||||
# A variant group is one slot of one core, whatever system each
|
||||
# member is filed under: PicoDrive files its US Mega CD BIOS under
|
||||
# sega-segacd and the EU/JP ones under sega-megacd, and find_bios
|
||||
# picks one of the twelve. Keyed as emulator mode keys it.
|
||||
group_ids = [f"{extra.get('source_emulator', '')}:variant:{variant}"]
|
||||
else:
|
||||
group_ids = _extra_system_ids(extra) or ["_extras"]
|
||||
for group_id in group_ids:
|
||||
groups.setdefault(group_id, []).append((dest, name))
|
||||
return groups, extra_dests
|
||||
|
||||
def emulator_region_drops(
|
||||
selected: list[tuple[str, dict]], standalone: bool, regions: list[str]
|
||||
) -> set[str]:
|
||||
"""Destinations an emulator-mode `--region` withdraws.
|
||||
|
||||
One group per system (or explicit variant_group) inside each profile, so
|
||||
a multi-system core such as O2EM keeps a fallback BIOS for every system
|
||||
instead of letting one matching system empty another. The emulator pack
|
||||
and `verify --emulator` both read this, keyed by pack destination.
|
||||
"""
|
||||
import region as region_mod
|
||||
from packpaths import _resolve_destination
|
||||
from validation import filter_files_by_mode
|
||||
|
||||
region_index = region_mod.build_region_index(dict(selected))
|
||||
region_groups: dict[str, list[tuple[str, str]]] = {}
|
||||
for emu_name, profile in sorted(selected):
|
||||
structure = profile.get("pack_structure")
|
||||
for fe in filter_files_by_mode(profile.get("files", []), standalone):
|
||||
dest = _resolve_destination(fe, structure, standalone)
|
||||
if dest:
|
||||
group_id = _emulator_region_group(emu_name, profile, fe)
|
||||
region_groups.setdefault(group_id, []).append(
|
||||
(dest, fe.get("name", ""))
|
||||
)
|
||||
return region_mod.resolve_region_drops(region_groups, region_index, regions)
|
||||
|
||||
|
||||
def _emulator_region_group(emu_name: str, profile: dict, file_entry: dict) -> str:
|
||||
"""Stable group ID for regional alternatives within an emulator profile."""
|
||||
variant = file_entry.get("variant_group")
|
||||
|
||||
+5
-49
@@ -8,7 +8,6 @@ from __future__ import annotations
|
||||
|
||||
import re
|
||||
_HEX_RE = re.compile(r"\b([0-9a-fA-F]{8,40})\b")
|
||||
from validation import check_file_validation
|
||||
from largefiles import fetch_large_file
|
||||
import os
|
||||
from hashing import parse_md5_list
|
||||
@@ -117,54 +116,6 @@ def lookup_hashes(
|
||||
pass
|
||||
print(f" In repo: {'YES' if in_repo else 'NO'}")
|
||||
|
||||
def _find_candidate_satisfying_both(
|
||||
file_entry: dict,
|
||||
db: dict,
|
||||
local_path: str,
|
||||
validation_index: dict,
|
||||
bios_dir: str,
|
||||
) -> str | None:
|
||||
"""Search for a repo file that satisfies both platform MD5 and emulator validation.
|
||||
|
||||
When the current file passes platform verification but fails emulator checks,
|
||||
search all candidates with the same name for one that passes both.
|
||||
Returns a better path, or None if no upgrade found.
|
||||
"""
|
||||
fname = file_entry.get("name", "")
|
||||
if not fname:
|
||||
return None
|
||||
entry = validation_index.get(fname)
|
||||
if not entry:
|
||||
return None
|
||||
|
||||
md5_expected = file_entry.get("md5", "")
|
||||
md5_set = (
|
||||
{m.strip().lower() for m in md5_expected.split(",") if m.strip()}
|
||||
if md5_expected
|
||||
else set()
|
||||
)
|
||||
|
||||
by_name = db.get("indexes", {}).get("by_name", {})
|
||||
files_db = db.get("files", {})
|
||||
|
||||
for sha1 in by_name.get(fname, []):
|
||||
candidate = files_db.get(sha1, {})
|
||||
path = candidate.get("path", "")
|
||||
if (
|
||||
not path
|
||||
or not os.path.exists(path)
|
||||
or os.path.realpath(path) == os.path.realpath(local_path)
|
||||
):
|
||||
continue
|
||||
# Must still satisfy platform MD5
|
||||
if md5_set and candidate.get("md5", "").lower() not in md5_set:
|
||||
continue
|
||||
# Check emulator validation
|
||||
reason = check_file_validation(path, fname, validation_index, bios_dir)
|
||||
if reason is None:
|
||||
return path
|
||||
return None
|
||||
|
||||
def resolve_file(
|
||||
file_entry: dict,
|
||||
db: dict,
|
||||
@@ -204,6 +155,11 @@ def resolve_file(
|
||||
first_sha1 = (sha1[0] if sha1 else "") if isinstance(sha1, list) else (sha1 or "")
|
||||
md5_list = parse_md5_list(file_entry.get("md5"))
|
||||
first_md5 = md5_list[0] if md5_list else ""
|
||||
if file_entry.get("unsourceable") and not (first_sha1 or first_md5):
|
||||
# The asset store is addressed by file name: with no hash to check,
|
||||
# it would hand back the same-named file of another game, which is
|
||||
# exactly what the flag forbids.
|
||||
return (path, status) if path else (None, "not_found")
|
||||
cached = fetch_large_file(
|
||||
name,
|
||||
expected_sha1=first_sha1,
|
||||
|
||||
@@ -361,6 +361,19 @@ def _repo_satisfies_declaration(
|
||||
continue
|
||||
return False
|
||||
|
||||
def _repo_holds_member(file_entry: dict, db: dict) -> bool:
|
||||
"""Whether a held archive of that name carries the declared member."""
|
||||
files_db = db.get("files", {})
|
||||
declared = [m.strip() for m in str(file_entry.get("md5") or "").split(",") if m.strip()]
|
||||
for sha1 in db.get("indexes", {}).get("by_name", {}).get(file_entry.get("name", ""), []):
|
||||
path = (files_db.get(sha1) or {}).get("path", "")
|
||||
if not path.endswith(".zip") or not os.path.exists(path):
|
||||
continue
|
||||
if any(check_inside_zip(path, file_entry["zipped_file"], m) == "ok" for m in declared):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _intentional_hash_exclusion(
|
||||
entries: list[dict],
|
||||
db: dict,
|
||||
@@ -501,6 +514,7 @@ def verify_pack_against_platform(
|
||||
db,
|
||||
base_dest,
|
||||
emu_profiles,
|
||||
target_cores=target_cores,
|
||||
)
|
||||
region_drops = region_mod.resolve_region_drops(
|
||||
region_groups, region_index, regions
|
||||
@@ -586,6 +600,16 @@ def verify_pack_against_platform(
|
||||
]
|
||||
if not checkable:
|
||||
continue
|
||||
# An archive declared once per ROM it must hold is checked
|
||||
# once per ROM by the frontend: each such declaration has to
|
||||
# pass on its own, where the repository holds an archive
|
||||
# that would.
|
||||
for fe in checkable:
|
||||
if not fe.get("zipped_file"):
|
||||
continue
|
||||
err = _check_member_hash(zf, member, fe, digest)
|
||||
if err is not None and (db is None or _repo_holds_member(fe, db)):
|
||||
errors.append(err)
|
||||
member_errors = []
|
||||
satisfied = False
|
||||
for fe in checkable:
|
||||
|
||||
@@ -249,6 +249,8 @@ def main():
|
||||
help="Export native formats (implies --with-truth)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
if args.all_variants and args.source != "full":
|
||||
parser.error("--source is incompatible with --all-variants")
|
||||
|
||||
# A second run on the same output directory is refused before any work:
|
||||
# the database rebuild alone takes minutes, and the reader holding the
|
||||
|
||||
+107
-23
@@ -459,6 +459,34 @@ def _prose_external(line: str, path_start: int, known=frozenset()) -> bool:
|
||||
return not lead or lead[-1] in ",:("
|
||||
|
||||
|
||||
def resolve_project_word(path: str, trees) -> str | None:
|
||||
"""Path of `project file` inside a declared tree, None when out of reach.
|
||||
|
||||
m64plus-fz cites `rice Config.cpp` and `mupen64plus-core
|
||||
device/pif/bootrom_hle.c`: plugins vendored in the declared repository
|
||||
under mupen64plus-video-rice/ and mupen64plus-core/. A citation is in
|
||||
reach when exactly one file ends with the path below a directory naming
|
||||
the project; the first tree that answers decides.
|
||||
"""
|
||||
word, _, rest = path.partition(" ")
|
||||
rest = rest.strip().lstrip("/")
|
||||
if not rest:
|
||||
return None
|
||||
word = word.lower()
|
||||
suffix = "/" + rest
|
||||
for tree in trees:
|
||||
matches = [
|
||||
p for p in tree or []
|
||||
if p.endswith(suffix)
|
||||
and any(word in seg.lower() for seg in p[: -len(suffix)].split("/"))
|
||||
]
|
||||
if len(matches) == 1:
|
||||
return matches[0]
|
||||
if matches:
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
def _prose_runs(text: str, known=frozenset()):
|
||||
"""Citation runs inside one prose scalar.
|
||||
|
||||
@@ -491,8 +519,14 @@ def _prose_runs(text: str, known=frozenset()):
|
||||
path_start -= 1
|
||||
path = "." + path
|
||||
if _prose_external(line, path_start, known):
|
||||
consumed = match.end()
|
||||
continue
|
||||
# Kept with its project word, the source_ref convention: the
|
||||
# report looks for the project inside the declared trees
|
||||
# (mupen64plus-ae vendors `rice Config.cpp`) and calls the
|
||||
# citation EXTERNAL only when none carries it.
|
||||
before = line[:path_start].rstrip()
|
||||
word = before[max(before.rfind(" "), before.rfind("\t"),
|
||||
before.rfind("(")) + 1:]
|
||||
path = f"{word} {path}"
|
||||
path_span = (path_start, match.end("path"))
|
||||
spans = [(match.start("range"), match.end("range"))]
|
||||
end = match.end()
|
||||
@@ -1511,6 +1545,27 @@ def build_report(
|
||||
candidates = near
|
||||
return None, candidates
|
||||
|
||||
def _tree_order():
|
||||
for view in views:
|
||||
yield _pin_tree_for(view)
|
||||
for view in views:
|
||||
yield _context_for(view)[1]
|
||||
|
||||
resolved_words: dict[str, str | None] = {}
|
||||
for index, (display, ref, tokens, hashes, parts, citation) in enumerate(refs):
|
||||
rewritten = []
|
||||
for part in parts:
|
||||
if is_external_citation(part.path):
|
||||
if part.path not in resolved_words:
|
||||
resolved_words[part.path] = resolve_project_word(
|
||||
part.path, _tree_order()
|
||||
)
|
||||
found = resolved_words[part.path]
|
||||
if found:
|
||||
part = RefPart(found, part.start, part.end, part.raw)
|
||||
rewritten.append(part)
|
||||
refs[index] = (display, ref, tokens, hashes, rewritten, citation)
|
||||
|
||||
lines_cache: dict[tuple[str, str, int | None], list[str] | None] = {}
|
||||
|
||||
def fetch(which: str, path: str, start: int | None = None, tokens=(), forced=None):
|
||||
@@ -2045,11 +2100,18 @@ def _prose_moves(
|
||||
return moves
|
||||
|
||||
|
||||
def _run_after_moves(parts, moves: dict[int, tuple[int, int, str | None]]) -> str:
|
||||
"""The prose run as it reads once its moved ranges are rewritten."""
|
||||
def _run_after_moves(
|
||||
parts, moves: dict[int, tuple[int, int, str | None]], run: str = ""
|
||||
) -> str:
|
||||
"""The prose run as it reads once its moved ranges are rewritten.
|
||||
|
||||
The path is the one written in the run: a part can carry a project word
|
||||
or the tree path the report resolved it to, neither of which the
|
||||
sentence spells.
|
||||
"""
|
||||
new_path = next(
|
||||
(move[2] for move in moves.values() if move[2]), None
|
||||
)
|
||||
) or (run.split(":", 1)[0] if run else None)
|
||||
ranges = []
|
||||
for index, part in enumerate(parts):
|
||||
if index in moves:
|
||||
@@ -2126,7 +2188,7 @@ def _apply_prose_edits(
|
||||
low = pattern.search(lines[run_hits[0]]).start()
|
||||
file_edits.setdefault(run_hits[0], []).append(
|
||||
(low, low + len(citation.ref),
|
||||
_run_after_moves(citation.parts, moves))
|
||||
_run_after_moves(citation.parts, moves, citation.ref))
|
||||
)
|
||||
slot = (id(citation.holder), citation.key)
|
||||
holders[slot] = (citation.holder, citation.key)
|
||||
@@ -2135,7 +2197,7 @@ def _apply_prose_edits(
|
||||
)
|
||||
applied.append(
|
||||
f"{citation.field}: {citation.ref} -> "
|
||||
f"{_run_after_moves(citation.parts, moves)}"
|
||||
f"{_run_after_moves(citation.parts, moves, citation.ref)}"
|
||||
)
|
||||
|
||||
for number, replacements in file_edits.items():
|
||||
@@ -2334,7 +2396,9 @@ def pending_recale(
|
||||
):
|
||||
pending += 1
|
||||
continue
|
||||
rendered = _run_after_moves([p.part for p in entry.parts], moves)
|
||||
rendered = _run_after_moves(
|
||||
[p.part for p in entry.parts], moves, entry.source_ref
|
||||
)
|
||||
if _citation_key(rendered) == _citation_key(entry.source_ref):
|
||||
continue
|
||||
slot = (entry.field, _citation_key(rendered))
|
||||
@@ -2413,25 +2477,30 @@ def _scalar_values(document: dict) -> dict[str, str]:
|
||||
}
|
||||
|
||||
|
||||
def _writing_pin(
|
||||
def _writing_pins(
|
||||
revisions: list[tuple[str, dict]], intro_sha: str, field: str
|
||||
) -> str | None:
|
||||
"""Pin recorded at the introducing commit, or first recorded after it.
|
||||
) -> list[str]:
|
||||
"""Distinct pins committed while the scalar held its current text.
|
||||
|
||||
The backfill resolved missing pins from profiled_date, the writing time
|
||||
of the profile, so the first value ever recorded stands for the scalars
|
||||
that predate it.
|
||||
The pin at the introducing commit stands for the scalars that predate
|
||||
it (the backfill resolved missing pins from profiled_date). A pin that
|
||||
moved later under the same text is either a bump that left the prose
|
||||
behind or a correction putting the pin on the revision the text already
|
||||
described: vitaquake2 was written under 34f0888, then re-pinned to
|
||||
59a5115 by a commit that touched nothing else. The history cannot tell
|
||||
the two apart, so every pin the text lived under is returned.
|
||||
"""
|
||||
index = next(
|
||||
(i for i, (sha, _) in enumerate(revisions) if sha == intro_sha), None
|
||||
)
|
||||
if index is None:
|
||||
return None
|
||||
return []
|
||||
pins: list[str] = []
|
||||
for position in range(index, -1, -1):
|
||||
value = revisions[position][1].get(field)
|
||||
if isinstance(value, str) and value:
|
||||
return value
|
||||
return None
|
||||
if isinstance(value, str) and value and value not in pins:
|
||||
pins.append(value)
|
||||
return pins
|
||||
|
||||
|
||||
def _resolve_at(
|
||||
@@ -2473,6 +2542,8 @@ def _realign_part(
|
||||
part: RefPart, pairs, cache_dir: str, offline: bool
|
||||
) -> tuple[str, object] | None:
|
||||
"""One range, anchored from its writing revision to the current pin."""
|
||||
if is_external_citation(part.path):
|
||||
return None
|
||||
unclear = None
|
||||
for repo, written, current in pairs:
|
||||
state, payload = _resolve_at(repo, written, part.path, cache_dir, offline)
|
||||
@@ -2509,7 +2580,8 @@ def realign_prose(
|
||||
anchoring it from the current pin would faithfully track the wrong
|
||||
content: the cited line holds someone else's code there. The writing pin
|
||||
is read from the profile's own history, at the commit that introduced
|
||||
the scalar's current text.
|
||||
the scalar's current text, and only when no other pin was committed
|
||||
under that text since.
|
||||
"""
|
||||
text = path.read_text(encoding="utf-8")
|
||||
document = yaml.safe_load(text)
|
||||
@@ -2558,13 +2630,25 @@ def realign_prose(
|
||||
# The scalar is not committed yet: written now, under this pin.
|
||||
continue
|
||||
pairs = []
|
||||
ambiguous: list[str] = []
|
||||
for pin_field, repo in repos:
|
||||
current = document.get(f"{pin_field}_commit")
|
||||
if not isinstance(current, str) or not current:
|
||||
continue
|
||||
written = _writing_pin(revisions, intro_sha, f"{pin_field}_commit")
|
||||
if written and written != current:
|
||||
pairs.append((repo, written, current))
|
||||
written = _writing_pins(revisions, intro_sha, f"{pin_field}_commit")
|
||||
if len(written) > 1:
|
||||
ambiguous.append(f"{pin_field}_commit {' -> '.join(written)}")
|
||||
elif written and written[0] != current:
|
||||
pairs.append((repo, written[0], current))
|
||||
if ambiguous:
|
||||
# Moving from the wrong one rewrites a correct citation onto
|
||||
# someone else's code, with nothing to show it happened.
|
||||
messages.extend(
|
||||
f"read again: {citation.field}: {citation.ref} (pin moved under "
|
||||
f"this text: {item}; the history does not say which it describes)"
|
||||
for item in ambiguous
|
||||
)
|
||||
continue
|
||||
if not pairs:
|
||||
continue
|
||||
moves: dict[int, tuple[int, int, str | None]] = {}
|
||||
@@ -2593,7 +2677,7 @@ def realign_prose(
|
||||
if dry_run:
|
||||
messages.extend(
|
||||
f"would recale {citation.field}: {citation.ref} -> "
|
||||
f"{_run_after_moves(citation.parts, moves)}"
|
||||
f"{_run_after_moves(citation.parts, moves, citation.ref)}"
|
||||
for citation, moves in jobs
|
||||
)
|
||||
return messages
|
||||
|
||||
@@ -13,6 +13,7 @@ Usage:
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import contextlib
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
@@ -50,6 +51,28 @@ def load_registry(registry_path: str = DEFAULT_REGISTRY) -> dict[str, dict]:
|
||||
return data.get("data_directories", {})
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _file_lock(lock_path: Path):
|
||||
"""Hold an exclusive lock on lock_path, waiting for it if taken.
|
||||
|
||||
Several sessions refresh the same data directories: without it, two
|
||||
swaps of one tree interleave and the second lands inside the first.
|
||||
On platforms without flock the lock is a no-op.
|
||||
"""
|
||||
try:
|
||||
import fcntl
|
||||
except ImportError:
|
||||
yield
|
||||
return
|
||||
lock_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(lock_path, "w") as handle:
|
||||
fcntl.flock(handle, fcntl.LOCK_EX)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
fcntl.flock(handle, fcntl.LOCK_UN)
|
||||
|
||||
|
||||
def _load_versions(versions_path: str = VERSIONS_FILE) -> dict[str, dict]:
|
||||
path = Path(versions_path)
|
||||
if not path.exists():
|
||||
@@ -61,11 +84,65 @@ def _load_versions(versions_path: str = VERSIONS_FILE) -> dict[str, dict]:
|
||||
def _save_versions(
|
||||
versions: dict[str, dict], versions_path: str = VERSIONS_FILE
|
||||
) -> None:
|
||||
"""Write the version file whole or not at all.
|
||||
|
||||
Truncating in place let a concurrent reader load an empty file and die
|
||||
on JSONDecodeError. The scratch file sits beside the target so the
|
||||
rename stays on one filesystem.
|
||||
"""
|
||||
path = Path(versions_path)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(path, "w") as f:
|
||||
json.dump(versions, f, indent=2, sort_keys=True)
|
||||
f.write("\n")
|
||||
handle, scratch = tempfile.mkstemp(
|
||||
dir=path.parent, prefix=f".{path.name}.", suffix=".tmp"
|
||||
)
|
||||
try:
|
||||
with os.fdopen(handle, "w") as f:
|
||||
json.dump(versions, f, indent=2, sort_keys=True)
|
||||
f.write("\n")
|
||||
os.replace(scratch, path)
|
||||
except BaseException:
|
||||
with contextlib.suppress(OSError):
|
||||
os.unlink(scratch)
|
||||
raise
|
||||
|
||||
|
||||
def _record_version(key: str, record: dict, versions_path: str) -> None:
|
||||
"""Set one key under the file lock, so two writers keep both updates."""
|
||||
path = Path(versions_path)
|
||||
with _file_lock(path.with_name(f".{path.name}.lock")):
|
||||
versions = _load_versions(versions_path)
|
||||
versions[key] = record
|
||||
_save_versions(versions, versions_path)
|
||||
|
||||
|
||||
def _staging_dir(cache_dir: Path) -> tempfile.TemporaryDirectory:
|
||||
"""Scratch space beside the cache, on its filesystem.
|
||||
|
||||
/tmp is a 4 GB tmpfs here: staging there filled it and turned the
|
||||
promotion into a copy, during which the cache held a partial tree.
|
||||
"""
|
||||
cache_dir.parent.mkdir(parents=True, exist_ok=True)
|
||||
return tempfile.TemporaryDirectory(
|
||||
dir=cache_dir.parent, prefix=f".{cache_dir.name}-"
|
||||
)
|
||||
|
||||
|
||||
def _promote(extract_dir: Path, cache_dir: Path, scratch: Path) -> None:
|
||||
"""Swap the new tree in by renames alone.
|
||||
|
||||
The old tree steps aside into the run's own scratch directory, never a
|
||||
fixed sibling name another run could be using, and comes back if the
|
||||
new one cannot be moved in.
|
||||
"""
|
||||
previous = scratch / "previous"
|
||||
if cache_dir.exists():
|
||||
os.replace(cache_dir, previous)
|
||||
try:
|
||||
os.replace(extract_dir, cache_dir)
|
||||
except BaseException:
|
||||
if previous.exists() and not cache_dir.exists():
|
||||
os.replace(previous, cache_dir)
|
||||
raise
|
||||
|
||||
|
||||
def _api_request(url: str) -> dict:
|
||||
@@ -149,7 +226,7 @@ def _download_and_extract(
|
||||
exclude = exclude or []
|
||||
cache_dir = Path(local_cache)
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
with _staging_dir(cache_dir) as tmpdir:
|
||||
tarball_path = Path(tmpdir) / "archive.tar.gz"
|
||||
log.info("downloading %s", source_url)
|
||||
|
||||
@@ -206,22 +283,7 @@ def _download_and_extract(
|
||||
shutil.copyfileobj(src, dst)
|
||||
file_count += 1
|
||||
|
||||
# atomic swap: rename old before moving new into place
|
||||
cache_dir.parent.mkdir(parents=True, exist_ok=True)
|
||||
old_cache = cache_dir.with_suffix(".old")
|
||||
if cache_dir.exists():
|
||||
if old_cache.exists():
|
||||
shutil.rmtree(old_cache)
|
||||
cache_dir.rename(old_cache)
|
||||
try:
|
||||
shutil.move(str(extract_dir), str(cache_dir))
|
||||
except OSError:
|
||||
# Restore old cache on failure
|
||||
if old_cache.exists() and not cache_dir.exists():
|
||||
old_cache.rename(cache_dir)
|
||||
raise
|
||||
if old_cache.exists():
|
||||
shutil.rmtree(old_cache)
|
||||
_promote(extract_dir, cache_dir, Path(tmpdir))
|
||||
|
||||
return file_count
|
||||
|
||||
@@ -241,7 +303,7 @@ def _download_and_extract_zip(
|
||||
exclude = exclude or []
|
||||
cache_dir = Path(local_cache)
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
with _staging_dir(cache_dir) as tmpdir:
|
||||
zip_path = Path(tmpdir) / "archive.zip"
|
||||
log.info("downloading %s", source_url)
|
||||
|
||||
@@ -285,21 +347,7 @@ def _download_and_extract_zip(
|
||||
# The old tree is stepped aside rather than deleted: removing it
|
||||
# first and then failing to move the new one in left the cache with
|
||||
# nothing at all, and the next run reads that as "never fetched".
|
||||
cache_dir.parent.mkdir(parents=True, exist_ok=True)
|
||||
previous = None
|
||||
if cache_dir.exists():
|
||||
previous = cache_dir.with_name(cache_dir.name + ".previous")
|
||||
if previous.exists():
|
||||
shutil.rmtree(previous)
|
||||
os.replace(cache_dir, previous)
|
||||
try:
|
||||
shutil.move(str(extract_dir), str(cache_dir))
|
||||
except BaseException:
|
||||
if previous is not None:
|
||||
os.replace(previous, cache_dir)
|
||||
raise
|
||||
if previous is not None:
|
||||
shutil.rmtree(previous, ignore_errors=True)
|
||||
_promote(extract_dir, cache_dir, Path(tmpdir))
|
||||
|
||||
return file_count
|
||||
|
||||
@@ -327,7 +375,20 @@ def refresh_entry(
|
||||
"""Refresh a single data directory entry.
|
||||
|
||||
Returns True if the entry was refreshed (or would be in dry-run mode).
|
||||
The decision and the swap run under one lock per directory: a run that
|
||||
waited for another session re-reads the version it recorded and finds
|
||||
nothing left to do.
|
||||
"""
|
||||
if dry_run:
|
||||
return _refresh_entry(key, entry, force, dry_run, versions_path)
|
||||
cache_dir = Path(entry["local_cache"])
|
||||
with _file_lock(cache_dir.with_name(f".{cache_dir.name}.lock")):
|
||||
return _refresh_entry(key, entry, force, dry_run, versions_path)
|
||||
|
||||
|
||||
def _refresh_entry(
|
||||
key: str, entry: dict, force: bool, dry_run: bool, versions_path: str
|
||||
) -> bool | None:
|
||||
source_type = entry.get("source_type", "tarball")
|
||||
version = entry.get("version", "master")
|
||||
source_url = entry["source_url"].format(version=version)
|
||||
@@ -389,9 +450,7 @@ def refresh_entry(
|
||||
remote_tag = _get_remote_etag(source_url)
|
||||
else:
|
||||
remote_tag = get_remote_sha(entry["source_url"], version)
|
||||
versions = _load_versions(versions_path)
|
||||
versions[key] = {"sha": remote_tag or "", "version": version}
|
||||
_save_versions(versions, versions_path)
|
||||
_record_version(key, {"sha": remote_tag or "", "version": version}, versions_path)
|
||||
|
||||
log.info("[%s] refreshed: %d files extracted to %s", key, file_count, local_cache)
|
||||
return True
|
||||
|
||||
@@ -60,6 +60,7 @@ SYSTEM_SLUG_MAP = {
|
||||
"Mattel - Intellivision": "mattel-intellivision",
|
||||
"Microsoft - MSX": "microsoft-msx",
|
||||
"NEC - PC Engine - TurboGrafx 16 - SuperGrafx": "nec-pc-engine",
|
||||
"NEC - PC-8801": "nec-pc-88",
|
||||
"NEC - PC-98": "nec-pc-98",
|
||||
"NEC - PC-FX": "nec-pc-fx",
|
||||
"Nintendo - Famicom Disk System": "nintendo-fds",
|
||||
@@ -69,12 +70,14 @@ SYSTEM_SLUG_MAP = {
|
||||
"Nintendo - Gameboy Color": "nintendo-gbc",
|
||||
"Nintendo - Nintendo 64DD": "nintendo-64dd",
|
||||
"Nintendo - Nintendo DS": "nintendo-ds",
|
||||
"Nintendo - Nintendo DSi": "nintendo-dsi",
|
||||
"Nintendo - Nintendo Entertainment System": "nintendo-nes",
|
||||
"Nintendo - Pokemon Mini": "nintendo-pokemon-mini",
|
||||
"Nintendo - Satellaview": "nintendo-satellaview",
|
||||
"Nintendo - SuFami Turbo": "nintendo-sufami-turbo",
|
||||
"Nintendo - Super Game Boy": "nintendo-sgb",
|
||||
"Nintendo - Super Nintendo Entertainment System": "nintendo-snes",
|
||||
"Philips - CD-i": "philips-cdi",
|
||||
"Phillips - Videopac+": "philips-videopac",
|
||||
"SNK - NeoGeo CD": "snk-neogeo-cd",
|
||||
"ScummVM": "scummvm",
|
||||
@@ -259,12 +262,6 @@ class Scraper(BaseScraper):
|
||||
# shared groups in _shared.yml. The includes directive is resolved
|
||||
# at load time by load_platform_config().
|
||||
EXTRA_SYSTEMS = {
|
||||
"nec-pc-88": {
|
||||
"includes": ["quasi88"],
|
||||
"core": "quasi88",
|
||||
"manufacturer": "NEC",
|
||||
"docs": "https://docs.libretro.com/library/quasi88/",
|
||||
},
|
||||
# ref: Vircon32/libretro.c -virtual console, single BIOS
|
||||
"vircon32": {
|
||||
"files": [
|
||||
@@ -470,6 +467,7 @@ class Scraper(BaseScraper):
|
||||
# Verified in fuse-libretro/src/compat/paths.c -core searches
|
||||
# system/ flat, not fuse/ subfolder. Docs are wrong on this.
|
||||
SYSTEM_SHARED_GROUPS = {
|
||||
"nec-pc-88": ["quasi88"],
|
||||
"nec-pc-98": ["np2kai"],
|
||||
"sharp-x68000": ["keropi"],
|
||||
"sega-saturn": ["kronos"],
|
||||
|
||||
+86
-1
@@ -21,7 +21,9 @@ from dataclasses import dataclass, field
|
||||
import nativemode
|
||||
from common import (
|
||||
build_zip_contents_index,
|
||||
check_inside_zip,
|
||||
load_data_dir_registry,
|
||||
parse_md5_list,
|
||||
resolution_is_hash_exact,
|
||||
resolve_local_file,
|
||||
runs_standalone,
|
||||
@@ -193,6 +195,13 @@ def profile_claims(
|
||||
return claims
|
||||
|
||||
|
||||
def _claim_rank(claim: Claim) -> int:
|
||||
"""How strongly a platform claim speaks for what the pack ships."""
|
||||
if resolution_is_hash_exact(claim.status):
|
||||
return 2
|
||||
return 1 if claim.is_proven else 0
|
||||
|
||||
|
||||
def find_conflicts(
|
||||
config: dict,
|
||||
profiles: dict,
|
||||
@@ -211,7 +220,13 @@ def find_conflicts(
|
||||
for claim in platform_claims(
|
||||
config, db, base_dest, zip_contents, data_dir_registry
|
||||
):
|
||||
by_dest.setdefault(_normalize(claim.destination), claim)
|
||||
key = _normalize(claim.destination)
|
||||
held = by_dest.get(key)
|
||||
# The pack ships the declaration a hash proves (_preferred_entries),
|
||||
# so a bare sibling met first must not stand for the destination:
|
||||
# it would hide the contradiction with the file actually shipped.
|
||||
if held is None or _claim_rank(claim) > _claim_rank(held):
|
||||
by_dest[key] = claim
|
||||
|
||||
# Grouped before judging: a profile may declare several revisions that are
|
||||
# all acceptable at one destination, and the platform choosing one of them
|
||||
@@ -264,6 +279,46 @@ class Decision:
|
||||
return self.reason == SERVES_BOTH
|
||||
|
||||
|
||||
def pack_overrides(
|
||||
config: dict,
|
||||
profiles: dict,
|
||||
db: dict,
|
||||
zip_contents: dict | None = None,
|
||||
data_dir_registry: dict | None = None,
|
||||
) -> dict[str, str]:
|
||||
"""Full pack destination -> the file a platform pack serves there instead.
|
||||
|
||||
Where a source-verified profile contradicts the scraped baseline on one
|
||||
destination, the pack answers to the platform it is built for; in
|
||||
existence mode the frontend never reads the bytes, so serving the
|
||||
emulator's file satisfies both. The ZIP builder, the install manifest and
|
||||
verify all read this: two of them deciding alone gave the one-line
|
||||
installer different bytes than the ZIP.
|
||||
"""
|
||||
from common import resolve_platform_cores
|
||||
|
||||
if not profiles:
|
||||
return {}
|
||||
platform_profiles = {
|
||||
name: profiles[name] for name in resolve_platform_cores(config, profiles)
|
||||
}
|
||||
mode = config.get("verification_mode", "existence")
|
||||
overrides: dict[str, str] = {}
|
||||
for conflict in find_conflicts(
|
||||
config,
|
||||
platform_profiles,
|
||||
db,
|
||||
config.get("base_destination", ""),
|
||||
{str(c) for c in config.get("standalone_cores", [])},
|
||||
zip_contents,
|
||||
data_dir_registry,
|
||||
):
|
||||
decision = arbitrate(conflict, mode)
|
||||
if decision.serves_both and decision.winner.local_path:
|
||||
overrides[conflict.destination] = decision.winner.local_path
|
||||
return overrides
|
||||
|
||||
|
||||
def arbitrate(conflict: Conflict, mode: str, addressee: str = "platform") -> Decision:
|
||||
"""Decide a contested destination for the pack being built.
|
||||
|
||||
@@ -362,10 +417,40 @@ def find_collisions(
|
||||
if local and local not in resolved:
|
||||
resolved.append(local)
|
||||
if len(resolved) > 1 and not _same_file_family(resolved):
|
||||
# A file every declaration accepts settles the path: RetroDECK
|
||||
# declares ATARIOSB.ROM twice, the second with an md5 list that
|
||||
# also names the first one's file.
|
||||
if any(_accepted_by_all(path, entries, db) for path in resolved):
|
||||
continue
|
||||
collisions.append(Collision(destination=key, resolved=resolved))
|
||||
return collisions
|
||||
|
||||
|
||||
def _accepted_by_all(path: str, entries: list[dict], db: dict) -> bool:
|
||||
"""Whether every declaration's own hashes accept the file at path."""
|
||||
record = next(
|
||||
(e for e in db.get("files", {}).values() if e.get("path") == path), None
|
||||
)
|
||||
if record is None:
|
||||
return False
|
||||
for entry in entries:
|
||||
if entry.get("zipped_file"):
|
||||
declared = parse_md5_list(entry.get("md5"))
|
||||
if declared and not any(
|
||||
check_inside_zip(path, entry["zipped_file"], m) == "ok" for m in declared
|
||||
):
|
||||
return False
|
||||
continue
|
||||
md5s = parse_md5_list(entry.get("md5"))
|
||||
if md5s and str(record.get("md5", "")).lower() not in md5s:
|
||||
return False
|
||||
sha1 = entry.get("sha1")
|
||||
sha1s = [sha1] if isinstance(sha1, str) else list(sha1 or [])
|
||||
if sha1s and str(record.get("sha1", "")).lower() not in {h.lower() for h in sha1s}:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _same_file_family(paths: list[str]) -> bool:
|
||||
"""Whether the paths are one file and its own pinned variants.
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ from __future__ import annotations
|
||||
import os
|
||||
|
||||
from common import compute_hashes
|
||||
from hashing import parse_md5_list
|
||||
|
||||
# Validation types that require console-specific cryptographic keys.
|
||||
# verify.py cannot reproduce these -size checks still apply if combined.
|
||||
@@ -321,3 +322,73 @@ def filter_files_by_mode(files: list[dict], standalone: bool) -> list[dict]:
|
||||
continue
|
||||
result.append(f)
|
||||
return result
|
||||
|
||||
|
||||
def find_validated_variant(
|
||||
file_entry: dict,
|
||||
db: dict,
|
||||
current_path: str,
|
||||
validation_index: dict,
|
||||
bios_dir: str = "bios",
|
||||
platform_digest: str | None = None,
|
||||
) -> str | None:
|
||||
"""A held file the emulator's own checks accept, in place of current_path.
|
||||
|
||||
Candidates come first from the hashes the emulator declares, which finds
|
||||
a dump stored under another name, then from the files sharing the name.
|
||||
platform_digest is the hash the frontend compares ("md5", "sha1") or None
|
||||
when it reads no bytes: a candidate must then also carry a value the
|
||||
entry declares, since the frontend would reject anything else. The
|
||||
report and the packs read this one function, so the file a report counts
|
||||
as satisfying the emulator is the file a pack ships.
|
||||
"""
|
||||
fname = file_entry.get("name", "")
|
||||
if not fname or fname not in validation_index:
|
||||
return None
|
||||
accepted: set[str] = set()
|
||||
if platform_digest:
|
||||
if file_entry.get("zipped_file"):
|
||||
return None
|
||||
declared = file_entry.get(platform_digest) or ""
|
||||
if platform_digest == "md5":
|
||||
accepted = set(parse_md5_list(declared))
|
||||
elif isinstance(declared, str) and declared:
|
||||
accepted = {declared.lower()}
|
||||
elif isinstance(declared, list):
|
||||
accepted = {str(value).lower() for value in declared}
|
||||
|
||||
files_db = db.get("files", {})
|
||||
indexes = db.get("indexes", {})
|
||||
current_real = os.path.realpath(current_path)
|
||||
seen: set[str] = set()
|
||||
|
||||
def candidates():
|
||||
expected = validation_index[fname]
|
||||
for hash_type, index_key in (
|
||||
("sha1", None), ("md5", "by_md5"), ("crc32", "by_crc32"), ("sha256", "by_sha256"),
|
||||
):
|
||||
for value in expected.get(hash_type) or []:
|
||||
if index_key is None:
|
||||
yield value
|
||||
continue
|
||||
found = indexes.get(index_key, {}).get(value)
|
||||
if isinstance(found, str):
|
||||
yield found
|
||||
elif isinstance(found, list):
|
||||
yield from found
|
||||
yield from indexes.get("by_name", {}).get(fname, [])
|
||||
|
||||
for sha1 in candidates():
|
||||
entry = files_db.get(sha1) or {}
|
||||
path = entry.get("path", "")
|
||||
if not path or not os.path.exists(path):
|
||||
continue
|
||||
real = os.path.realpath(path)
|
||||
if real == current_real or real in seen:
|
||||
continue
|
||||
seen.add(real)
|
||||
if accepted and str(entry.get(platform_digest, "")).lower() not in accepted:
|
||||
continue
|
||||
if check_file_validation(path, fname, validation_index, bios_dir) is None:
|
||||
return path
|
||||
return None
|
||||
+21
-126
@@ -71,6 +71,7 @@ from validation import (
|
||||
build_ground_truth,
|
||||
check_file_validation,
|
||||
filter_files_by_mode,
|
||||
find_validated_variant,
|
||||
)
|
||||
|
||||
DEFAULT_DB = "database.json"
|
||||
@@ -125,7 +126,7 @@ def verify_entry_existence(
|
||||
reason, emus_list = check
|
||||
suppressed = False
|
||||
if db:
|
||||
better = _find_best_variant(
|
||||
better = find_validated_variant(
|
||||
file_entry, db, local_path, validation_index,
|
||||
)
|
||||
if better:
|
||||
@@ -700,91 +701,6 @@ def find_exclusion_notes(
|
||||
# Platform verification
|
||||
|
||||
|
||||
def _find_best_variant(
|
||||
file_entry: dict,
|
||||
db: dict,
|
||||
current_path: str,
|
||||
validation_index: dict,
|
||||
) -> str | None:
|
||||
"""Search for a repo file that passes emulator validation.
|
||||
|
||||
Two-pass search:
|
||||
1. Hash lookup, using the emulator's expected hashes (sha1, md5, sha256,
|
||||
crc32) to find candidates directly in the DB indexes. This finds
|
||||
variants stored under different filenames (e.g. megacd2_v200_eu.bin
|
||||
for bios_CD_E.bin).
|
||||
2. Name lookup, checking all files sharing the same name (aliases,
|
||||
.variants/ with name-based suffixes).
|
||||
|
||||
If any candidate on disk passes ``check_file_validation``, the
|
||||
discrepancy is suppressed: the repo has what the emulator needs.
|
||||
"""
|
||||
fname = file_entry.get("name", "")
|
||||
if not fname or fname not in validation_index:
|
||||
return None
|
||||
|
||||
files_db = db.get("files", {})
|
||||
current_real = os.path.realpath(current_path)
|
||||
seen_paths: set[str] = set()
|
||||
|
||||
def _try_candidate(sha1: str) -> str | None:
|
||||
candidate = files_db.get(sha1, {})
|
||||
path = candidate.get("path", "")
|
||||
if not path or not os.path.exists(path):
|
||||
return None
|
||||
rp = os.path.realpath(path)
|
||||
if rp == current_real or rp in seen_paths:
|
||||
return None
|
||||
seen_paths.add(rp)
|
||||
if check_file_validation(path, fname, validation_index) is None:
|
||||
return path
|
||||
return None
|
||||
|
||||
# Pass 1: hash-based lookup from emulator expected values
|
||||
ventry = validation_index[fname]
|
||||
indexes = db.get("indexes", {})
|
||||
for hash_type, db_index_key in (
|
||||
("sha1", None),
|
||||
("md5", "by_md5"),
|
||||
("crc32", "by_crc32"),
|
||||
("sha256", "by_sha256"),
|
||||
):
|
||||
expected = ventry.get(hash_type)
|
||||
if not expected:
|
||||
continue
|
||||
if db_index_key is None:
|
||||
# SHA1 is the primary key of files_db
|
||||
for h in expected:
|
||||
if h in files_db:
|
||||
result = _try_candidate(h)
|
||||
if result:
|
||||
return result
|
||||
continue
|
||||
db_index = indexes.get(db_index_key, {})
|
||||
for h in expected:
|
||||
entries = db_index.get(h)
|
||||
if not entries:
|
||||
continue
|
||||
if isinstance(entries, list):
|
||||
for sha1 in entries:
|
||||
result = _try_candidate(sha1)
|
||||
if result:
|
||||
return result
|
||||
elif isinstance(entries, str):
|
||||
result = _try_candidate(entries)
|
||||
if result:
|
||||
return result
|
||||
|
||||
# Pass 2: name-based lookup (aliases, .variants/ with same filename)
|
||||
by_name = db.get("indexes", {}).get("by_name", {})
|
||||
for sha1 in by_name.get(fname, []):
|
||||
result = _try_candidate(sha1)
|
||||
if result:
|
||||
return result
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def verify_platform(
|
||||
config: dict,
|
||||
db: dict,
|
||||
@@ -814,28 +730,13 @@ def verify_platform(
|
||||
)
|
||||
zip_contents = build_zip_contents_index(db) if has_zipped else {}
|
||||
|
||||
slot_overrides: dict[str, str] = {}
|
||||
if emu_profiles:
|
||||
base_dest = config.get("base_destination", "")
|
||||
arbitrated = {
|
||||
name: emu_profiles[name]
|
||||
for name in resolve_platform_cores(config, emu_profiles)
|
||||
}
|
||||
for conflict in slots.find_conflicts(
|
||||
config,
|
||||
arbitrated,
|
||||
db,
|
||||
base_dest,
|
||||
{str(c) for c in config.get("standalone_cores", [])},
|
||||
zip_contents,
|
||||
data_dir_registry,
|
||||
):
|
||||
decision = slots.arbitrate(conflict, mode)
|
||||
if decision.serves_both and decision.winner.local_path:
|
||||
key = conflict.destination
|
||||
if base_dest and key.startswith(f"{base_dest}/"):
|
||||
key = key[len(base_dest) + 1:]
|
||||
slot_overrides[key] = decision.winner.local_path
|
||||
base_dest = config.get("base_destination", "")
|
||||
slot_overrides = {
|
||||
(key[len(base_dest) + 1:] if base_dest and key.startswith(f"{base_dest}/") else key): path
|
||||
for key, path in slots.pack_overrides(
|
||||
config, emu_profiles or {}, db, zip_contents, data_dir_registry
|
||||
).items()
|
||||
}
|
||||
|
||||
# Build HLE + validation indexes from emulator profiles
|
||||
profiles = (
|
||||
@@ -941,11 +842,12 @@ def verify_platform(
|
||||
)
|
||||
if check:
|
||||
reason, emus_list = check
|
||||
better = _find_best_variant(
|
||||
better = find_validated_variant(
|
||||
file_entry,
|
||||
db,
|
||||
local_path,
|
||||
validation_index,
|
||||
platform_digest=digest_algorithm(mode),
|
||||
)
|
||||
if not better:
|
||||
emus = ", ".join(emus_list)
|
||||
@@ -1325,7 +1227,7 @@ def verify_emulator(
|
||||
"""Verify files for specific emulator profiles.
|
||||
|
||||
A region priority list narrows the report the same way a pack built with the
|
||||
same list would be narrowed. One group per profile, as in generate_pack.
|
||||
same list would be narrowed, through the emulator pack's own drop set.
|
||||
"""
|
||||
load_emulator_profiles(emulators_dir)
|
||||
zip_contents = build_zip_contents_index(db)
|
||||
@@ -1348,22 +1250,12 @@ def verify_emulator(
|
||||
dest_to_name: dict[str, str] = {}
|
||||
data_dir_notices: list[str] = []
|
||||
|
||||
# The emulator pack withdraws these; the report withdraws the same.
|
||||
region_drops: set[str] = set()
|
||||
if regions:
|
||||
import region as region_mod
|
||||
from packextras import emulator_region_drops
|
||||
|
||||
region_index = region_mod.build_region_index(dict(selected))
|
||||
region_groups: dict[str, list[tuple[str, str]]] = {}
|
||||
for emu_name, profile in selected:
|
||||
members = region_groups.setdefault(emu_name, [])
|
||||
for fe in filter_files_by_mode(profile.get("files", []), standalone):
|
||||
nm = fe.get("name", "")
|
||||
key = fe.get("path") or nm
|
||||
if key:
|
||||
members.append((key, nm))
|
||||
region_drops = region_mod.resolve_region_drops(
|
||||
region_groups, region_index, regions
|
||||
)
|
||||
region_drops = emulator_region_drops(selected, standalone, regions)
|
||||
|
||||
for emu_name, profile in selected:
|
||||
files = expand_directory_entries(
|
||||
@@ -1372,10 +1264,13 @@ def verify_emulator(
|
||||
standalone,
|
||||
)
|
||||
if region_drops:
|
||||
from packpaths import _resolve_destination
|
||||
|
||||
structure = profile.get("pack_structure")
|
||||
files = [
|
||||
fe
|
||||
for fe in files
|
||||
if (fe.get("path") or fe.get("name", "")) not in region_drops
|
||||
if _resolve_destination(fe, structure, standalone) not in region_drops
|
||||
]
|
||||
|
||||
# Check data directories (only notice if not cached)
|
||||
@@ -1487,7 +1382,7 @@ def verify_emulator(
|
||||
|
||||
dest_hint = file_entry.get("path", "")
|
||||
local_path, resolve_status = resolve_local_file(
|
||||
file_entry,
|
||||
{**file_entry, "source_profile": emu_name},
|
||||
db,
|
||||
zip_contents,
|
||||
dest_hint=dest_hint,
|
||||
@@ -1514,7 +1409,7 @@ def verify_emulator(
|
||||
check = check_file_validation(local_path, name, validation_index)
|
||||
if check:
|
||||
reason, _emus = check
|
||||
better = _find_best_variant(
|
||||
better = find_validated_variant(
|
||||
file_entry, db, local_path, validation_index,
|
||||
)
|
||||
if better:
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
"""What a filename-agnostic core's scan may emit, and where.
|
||||
|
||||
yaps2 declares its BIOS under pcsx2/bios/ with a free name and its shaders,
|
||||
sounds and fonts under pcsx2/resources/ with fixed names. Every sized entry
|
||||
seeded the scan and every find went to the pack root: 189 resources landed
|
||||
flat in RetroArch's system/, and the BIOS the core reads from pcsx2/bios/
|
||||
was not where it looks.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(REPO_ROOT / "scripts"))
|
||||
|
||||
|
||||
class AgnosticScan(unittest.TestCase):
|
||||
def test_only_the_bios_directory_seeds_and_finds_keep_its_folder(self):
|
||||
from packextras import _agnostic_scan_extras
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
previous = os.getcwd()
|
||||
os.chdir(tmp)
|
||||
self.addCleanup(os.chdir, previous)
|
||||
files, by_name = {}, {}
|
||||
for path, data in (
|
||||
("bios/PS2/scph1.bin", b"a" * 64),
|
||||
("bios/PS2/scph2.bin", b"b" * 64),
|
||||
("bios/PS2/res/message.wav", b"c" * 8),
|
||||
("bios/PS2/res/other.wav", b"d" * 8),
|
||||
):
|
||||
Path(path).parent.mkdir(parents=True, exist_ok=True)
|
||||
Path(path).write_bytes(data)
|
||||
sha1 = hashlib.sha1(data).hexdigest()
|
||||
name = path.rsplit("/", 1)[1]
|
||||
files[sha1] = {"path": path, "name": name, "size": len(data), "sha1": sha1}
|
||||
by_name.setdefault(name, []).append(sha1)
|
||||
db = {"files": files, "indexes": {"by_name": by_name, "by_md5": {}, "by_crc32": {},
|
||||
"by_path_suffix": {}}}
|
||||
profile = {"emulator": "Y", "type": "libretro", "bios_mode": "agnostic",
|
||||
"bios_directory": "pcsx2/bios/", "systems": ["ps2"], "files": [
|
||||
{"name": "scph1.bin", "path": "pcsx2/bios/scph1.bin", "size": 64},
|
||||
{"name": "message.wav", "path": "pcsx2/resources/message.wav", "size": 8},
|
||||
]}
|
||||
extras = _agnostic_scan_extras({"y": profile}, {"y"}, db, by_name, set(), "")
|
||||
self.assertEqual(sorted(e["destination"] for e in extras),
|
||||
["pcsx2/bios/scph1.bin", "pcsx2/bios/scph2.bin"])
|
||||
|
||||
|
||||
class BundledHashIsProof(unittest.TestCase):
|
||||
"""A bundled file with a declared hash is served by those bytes or flagged.
|
||||
|
||||
Four NetherSX2-Turnip entries named their own sha1; the collection held
|
||||
NetherSX2's copies, which the builder shipped in their place.
|
||||
"""
|
||||
|
||||
def test_no_bundled_entry_resolves_to_other_bytes(self):
|
||||
if not (REPO_ROOT / "database.json").is_file():
|
||||
self.skipTest("no database.json")
|
||||
from common import (
|
||||
build_zip_contents_index,
|
||||
load_data_dir_registry,
|
||||
load_database,
|
||||
load_emulator_profiles,
|
||||
resolve_local_file,
|
||||
)
|
||||
|
||||
previous = os.getcwd()
|
||||
os.chdir(REPO_ROOT)
|
||||
self.addCleanup(os.chdir, previous)
|
||||
db = load_database("database.json")
|
||||
zips = build_zip_contents_index(db)
|
||||
registry = load_data_dir_registry("platforms")
|
||||
wrong = []
|
||||
for name, profile in sorted(load_emulator_profiles("emulators", skip_aliases=False).items()):
|
||||
for entry in profile.get("files") or []:
|
||||
if not entry.get("bundled") or entry.get("unsourceable"):
|
||||
continue
|
||||
if not any(entry.get(k) for k in ("sha1", "md5", "sha256", "crc32")):
|
||||
continue
|
||||
dest = entry.get("path") or entry.get("name") or ""
|
||||
local, status = resolve_local_file(
|
||||
{**entry, "source_profile": name}, db, zips, dest_hint=dest,
|
||||
data_dir_registry=registry)
|
||||
if local and status == "hash_mismatch":
|
||||
wrong.append((name, dest, local))
|
||||
self.assertEqual(wrong, [])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,113 @@
|
||||
"""An archive declared once per member ships as the copy holding them all.
|
||||
|
||||
Batocera checks adam_fdc.zip through eight zipped_file declarations. The
|
||||
first hash-exact answer was a one-member MAME set, so the pack shipped it and
|
||||
seven of the eight members Batocera checks were missing.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(REPO_ROOT / "scripts"))
|
||||
|
||||
|
||||
class PreferredArchive(unittest.TestCase):
|
||||
def test_the_archive_most_declarations_accept_wins(self):
|
||||
import generate_pack
|
||||
|
||||
roms = {f"r{i}.bin": bytes([i]) * 32 for i in range(3)}
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
previous = os.getcwd()
|
||||
os.chdir(tmp)
|
||||
self.addCleanup(os.chdir, previous)
|
||||
Path("bios/a").mkdir(parents=True)
|
||||
Path("bios/b").mkdir(parents=True)
|
||||
with zipfile.ZipFile("bios/a/set.zip", "w") as zf:
|
||||
zf.writestr("r0.bin", roms["r0.bin"])
|
||||
with zipfile.ZipFile("bios/b/set.zip", "w") as zf:
|
||||
for name, data in roms.items():
|
||||
zf.writestr(name, data)
|
||||
files, by_md5, by_name = {}, {}, {}
|
||||
for path in ("bios/a/set.zip", "bios/b/set.zip"):
|
||||
data = Path(path).read_bytes()
|
||||
sha1, md5 = hashlib.sha1(data).hexdigest(), hashlib.md5(data).hexdigest()
|
||||
files[sha1] = {"path": path, "name": "set.zip", "sha1": sha1, "md5": md5,
|
||||
"size": len(data)}
|
||||
by_md5[md5] = sha1
|
||||
by_name.setdefault("set.zip", []).append(sha1)
|
||||
db = {"files": files, "indexes": {"by_name": by_name, "by_md5": by_md5,
|
||||
"by_path_suffix": {}, "by_crc32": {}}}
|
||||
member_md5 = {n: hashlib.md5(d).hexdigest() for n, d in roms.items()}
|
||||
one_member = files[by_name["set.zip"][0]]["md5"]
|
||||
declarations = [{"name": "set.zip", "destination": "set.zip", "md5": one_member}]
|
||||
declarations += [
|
||||
{"name": "set.zip", "destination": "set.zip", "zipped_file": name, "md5": md5}
|
||||
for name, md5 in member_md5.items()
|
||||
]
|
||||
systems = {"s": {"files": declarations}}
|
||||
preferred = generate_pack._preferred_entries(
|
||||
systems, db, "bios", "", False, {}, None, True)
|
||||
chosen = next(fe for fe in declarations if id(fe) == preferred["set.zip"])
|
||||
path, _status = generate_pack.resolve_file(chosen, db, "bios", {}, offline=True)
|
||||
self.assertEqual(path, "bios/b/set.zip")
|
||||
|
||||
|
||||
class IntegrityChecksEveryMember(unittest.TestCase):
|
||||
"""A pack holding one ROM of an archive declared per ROM fails its check.
|
||||
|
||||
The check accepted the destination as soon as one declaration passed,
|
||||
so a Batocera pack missing seven of adam_fdc.zip's eight ROMs verified.
|
||||
"""
|
||||
|
||||
def test_a_missing_member_is_an_error_when_the_repository_holds_it(self):
|
||||
import yaml
|
||||
|
||||
import packverify
|
||||
|
||||
roms = {f"r{i}.bin": bytes([i]) * 32 for i in range(3)}
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
previous = os.getcwd()
|
||||
os.chdir(tmp)
|
||||
self.addCleanup(os.chdir, previous)
|
||||
Path("bios/b").mkdir(parents=True)
|
||||
with zipfile.ZipFile("bios/b/set.zip", "w") as zf:
|
||||
for name, data in roms.items():
|
||||
zf.writestr(name, data)
|
||||
data = Path("bios/b/set.zip").read_bytes()
|
||||
sha1 = hashlib.sha1(data).hexdigest()
|
||||
db = {"files": {sha1: {"path": "bios/b/set.zip", "name": "set.zip", "sha1": sha1,
|
||||
"md5": hashlib.md5(data).hexdigest(), "size": len(data)}},
|
||||
"indexes": {"by_name": {"set.zip": [sha1]}, "by_md5": {},
|
||||
"by_path_suffix": {}, "by_crc32": {}}}
|
||||
Path("platforms").mkdir()
|
||||
config = {"platform": "P", "verification_mode": "md5", "base_destination": "bios",
|
||||
"cores": [], "systems": {"s": {"files": [
|
||||
{"name": "set.zip", "destination": "set.zip", "zipped_file": n,
|
||||
"md5": hashlib.md5(d).hexdigest()} for n, d in roms.items()]}}}
|
||||
Path("platforms/p.yml").write_text(yaml.safe_dump(config), encoding="utf-8")
|
||||
Path("emulators").mkdir()
|
||||
import io
|
||||
|
||||
inner = io.BytesIO()
|
||||
with zipfile.ZipFile(inner, "w") as zf:
|
||||
zf.writestr("r0.bin", roms["r0.bin"])
|
||||
with zipfile.ZipFile("pack.zip", "w") as zf:
|
||||
zf.writestr("bios/set.zip", inner.getvalue())
|
||||
ok, *_rest, = packverify.verify_pack_against_platform(
|
||||
"pack.zip", "p", "platforms", db, emulators_dir="emulators", emu_profiles={})
|
||||
errors = _rest[2]
|
||||
self.assertFalse(ok)
|
||||
self.assertEqual(sorted(e.split(": ")[1] for e in errors),
|
||||
["r1.bin not found inside ZIP", "r2.bin not found inside ZIP"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,67 @@
|
||||
"""A profile's aliases attach to the file its name designates, or to none.
|
||||
|
||||
generate_db matched an alias-carrying entry by name through a dict that
|
||||
kept the last file of that name. quasi88's disk.rom aliases (N88SUB.ROM,
|
||||
n88sub.rom) went to a Tandy CoCo disk.rom, and a name lookup for the PC-88
|
||||
disk ROM then served the CoCo one.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import types
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(REPO_ROOT / "scripts"))
|
||||
|
||||
|
||||
class _NoCoreInfo:
|
||||
def fetch_requirements(self):
|
||||
return []
|
||||
|
||||
|
||||
class AliasesNeedAnUnambiguousName(unittest.TestCase):
|
||||
def _aliases(self, files: dict) -> dict:
|
||||
import generate_db
|
||||
|
||||
stub = types.ModuleType("scraper.coreinfo_scraper")
|
||||
stub.Scraper = _NoCoreInfo
|
||||
with tempfile.TemporaryDirectory(dir=REPO_ROOT / "tmp") as tmp:
|
||||
previous = os.getcwd()
|
||||
os.chdir(tmp)
|
||||
try:
|
||||
Path("emulators").mkdir()
|
||||
Path("emulators/x.yml").write_text(
|
||||
'files:\n - name: "disk.rom"\n aliases: ["n88sub.rom"]\n'
|
||||
)
|
||||
with mock.patch.dict(sys.modules, {"scraper.coreinfo_scraper": stub}):
|
||||
return generate_db._collect_all_aliases(files)
|
||||
finally:
|
||||
os.chdir(previous)
|
||||
|
||||
@staticmethod
|
||||
def _file(name: str, path: str, md5: str) -> dict:
|
||||
return {"name": name, "path": path, "md5": md5}
|
||||
|
||||
def test_homonyms_take_no_alias(self):
|
||||
files = {
|
||||
"a": self._file("disk.rom", "bios/NEC/PC-98/disk.rom", "m1"),
|
||||
"b": self._file("disk.rom", "bios/Tandy/CoCo/disk.rom", "m2"),
|
||||
}
|
||||
aliases = self._aliases(files)
|
||||
named = {a["name"] for entries in aliases.values() for a in entries}
|
||||
self.assertNotIn("n88sub.rom", named)
|
||||
|
||||
def test_a_unique_name_still_carries_its_aliases(self):
|
||||
files = {"a": self._file("disk.rom", "bios/NEC/PC-88/disk.rom", "m1")}
|
||||
aliases = self._aliases(files)
|
||||
self.assertEqual([a["name"] for a in aliases.get("a", [])], ["n88sub.rom"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -5956,6 +5956,29 @@ struct BurnDriver BurnDrvneogeo = {
|
||||
["--system", "sony-playstation"], "refuse"),
|
||||
(["--platform", "retroarch", "--split"],
|
||||
["--system", "sony-playstation"], "refuse"),
|
||||
# --all took the same filter and wrote twelve complete manifests.
|
||||
(["--all", "--manifest"], ["--system", "sony-playstation"], "refuse"),
|
||||
(["--all", "--split"], ["--system", "sony-playstation"], "refuse"),
|
||||
# A pack built with --system is named after its systems; the check
|
||||
# looked for the full pack instead.
|
||||
(["--platform", "retroarch", "--verify-packs"],
|
||||
["--system", "sony-playstation"], "refuse"),
|
||||
# --all-variants builds its own six combinations: it overrode these
|
||||
# and the single-pack modes never read it at all.
|
||||
(["--platform", "retroarch", "--all-variants"], ["--source", "truth"],
|
||||
"refuse"),
|
||||
(["--platform", "retroarch", "--all-variants"], ["--required-only"],
|
||||
"refuse"),
|
||||
(["--emulator", "duckstation"], ["--all-variants"], "refuse"),
|
||||
(["--system", "sony-playstation"], ["--all-variants"], "refuse"),
|
||||
(["--emulator", "duckstation", "--verify-packs"], ["--all-variants"],
|
||||
"refuse"),
|
||||
(["--all", "--manifest", "--all-variants"], ["--verify-packs"],
|
||||
"refuse"),
|
||||
(["--all", "--all-variants", "--verify-packs"], ["--one-per-slot"],
|
||||
"refuse"),
|
||||
(["--all", "--all-variants", "--verify-packs"], ["--split"],
|
||||
"refuse"),
|
||||
]
|
||||
for mode, flag, expected in matrix:
|
||||
with self.subTest(mode=mode, flag=flag):
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
"""An export writes what its summary announces, and nothing else.
|
||||
|
||||
RetroDECK announced 1474 additions and wrote none, counted 161 requirement
|
||||
corrections while keeping the platform's prose labels, and wrote one
|
||||
system's ATARIOSB.ROM over another's. RetroBat and EmuDeck turned empty or
|
||||
partial hash lists into content checks nobody counted, and a size was taken
|
||||
from a file other than the one the hash describes.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
import unittest
|
||||
from collections import OrderedDict
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(REPO_ROOT / "scripts"))
|
||||
|
||||
from exporter.baseline import NativeFile, NativeSystem, build_native_model # noqa: E402
|
||||
from exporter.emudeck_exporter import Exporter as EmuDeck # noqa: E402
|
||||
from exporter.retrobat_exporter import Exporter as RetroBat # noqa: E402
|
||||
from exporter.retrodeck_exporter import Exporter as RetroDeck # noqa: E402
|
||||
|
||||
A = "a" * 32
|
||||
B = "b" * 32
|
||||
C = "c" * 32
|
||||
|
||||
|
||||
class RetroDeckWritesWhatItCounts(unittest.TestCase):
|
||||
def test_same_name_in_two_systems_stays_two_entries(self):
|
||||
existing = [
|
||||
{"filename": "ATARIOSB.ROM", "system": "atari5200", "md5": f"{A},{B}"},
|
||||
{"filename": "ATARIOSB.ROM", "system": "atari800", "md5": B},
|
||||
]
|
||||
ours = [
|
||||
OrderedDict(filename="ATARIOSB.ROM", system="atari800", md5=C),
|
||||
]
|
||||
merged = RetroDeck._merge(existing, ours)
|
||||
by_system = {e["system"]: e["md5"] for e in merged}
|
||||
self.assertEqual(by_system, {"atari5200": f"{A},{B}", "atari800": C})
|
||||
self.assertEqual(len(merged), 2)
|
||||
|
||||
def test_a_list_of_systems_is_kept(self):
|
||||
existing = [{"filename": "neogeo.zip", "system": ["neogeo", "fbneo"], "md5": A}]
|
||||
ours = [OrderedDict(filename="neogeo.zip", system="fbneo", md5=B)]
|
||||
merged = RetroDeck._merge(existing, ours)
|
||||
self.assertEqual(len(merged), 1)
|
||||
self.assertEqual(merged[0]["system"], ["neogeo", "fbneo"])
|
||||
self.assertEqual(merged[0]["md5"], B)
|
||||
|
||||
def test_prose_label_is_neither_rewritten_nor_counted(self):
|
||||
exporter = RetroDeck()
|
||||
prose = NativeFile(
|
||||
"x.bin", "bios/x.bin", "psx",
|
||||
platform={"required": True, "required_label": "At least one BIOS file required"},
|
||||
truth={"required": False},
|
||||
)
|
||||
plain = NativeFile(
|
||||
"y.bin", "bios/y.bin", "psx",
|
||||
platform={"required": True, "required_label": "Required"},
|
||||
truth={"required": False},
|
||||
)
|
||||
self.assertFalse(exporter.states(prose, "required"))
|
||||
self.assertEqual(RetroDeck._entry(prose)["required"], "At least one BIOS file required")
|
||||
self.assertTrue(exporter.states(plain, "required"))
|
||||
self.assertEqual(RetroDeck._entry(plain)["required"], "Optional")
|
||||
|
||||
def test_addition_goes_to_the_system_component(self):
|
||||
declared = NativeFile("a.bin", "bios/a.bin", "psx",
|
||||
platform={"component": "duckstation", "md5": A})
|
||||
added = NativeFile("b.bin", "bios/b.bin", "psx", truth={"md5": B})
|
||||
spread = NativeSystem("amiga", files=[
|
||||
NativeFile("k1", "bios/k1", "amiga", platform={"component": "retroarch"}),
|
||||
NativeFile("k2", "bios/k2", "amiga", platform={"component": "puae"}),
|
||||
NativeFile("k3", "bios/k3", "amiga", truth={"md5": C}),
|
||||
])
|
||||
systems = {"psx": NativeSystem("psx", files=[declared, added]), "amiga": spread}
|
||||
manifest = json.dumps({"duckstation": {"name": "DuckStation", "bios": []}})
|
||||
produced = RetroDeck().render(
|
||||
systems, None, {"duckstation/component_manifest.json": manifest}
|
||||
)
|
||||
written = json.loads(produced["duckstation/component_manifest.json"])
|
||||
names = [e["filename"] for e in written["duckstation"]["bios"]]
|
||||
self.assertIn("b.bin", names)
|
||||
self.assertFalse(RetroDeck.writable(spread.files[2]))
|
||||
|
||||
|
||||
class HashesTheFormatCannotHold(unittest.TestCase):
|
||||
def test_retrobat_fills_only_a_single_accepted_value(self):
|
||||
exporter = RetroBat()
|
||||
single = NativeFile("s.bin", "s.bin", "nds", platform={"md5": ""},
|
||||
truth={"md5": A}, filled=["md5"])
|
||||
several = NativeFile("m.bin", "m.bin", "nds", platform={"md5": ""},
|
||||
truth={"md5": [A, B]}, filled=["md5"])
|
||||
self.assertEqual(RetroBat._md5(single), A)
|
||||
self.assertTrue(exporter.states(single, "md5"))
|
||||
self.assertEqual(RetroBat._md5(several), "")
|
||||
self.assertFalse(exporter.states(several, "md5"))
|
||||
|
||||
def test_emudeck_never_grows_an_array(self):
|
||||
unhashed = NativeFile("scph5501.bin", "scph5501.bin", "psx",
|
||||
platform={"md5": ""}, truth={"md5": C}, filled=["md5"])
|
||||
extra = NativeFile("scph1001.bin", "scph1001.bin", "psx",
|
||||
platform={"md5": A}, truth={"md5": [A, B]})
|
||||
corrected = NativeFile("scph7001.bin", "scph7001.bin", "psx",
|
||||
platform={"md5": B}, truth={"md5": C},
|
||||
corrections=["md5"])
|
||||
systems = {"psx": NativeSystem("psx", files=[unhashed, extra, corrected])}
|
||||
self.assertEqual(EmuDeck._md5s(systems, "psx"), [A, C])
|
||||
self.assertFalse(EmuDeck().states(unhashed, "md5"))
|
||||
|
||||
|
||||
class ModelKeepsOneFileOneEntry(unittest.TestCase):
|
||||
def test_size_describes_the_hash_written(self):
|
||||
fe = NativeFile("boot.bin", "dc/boot.bin", "dc",
|
||||
platform={"size": 2097152, "md5": A}, truth={"size": 480})
|
||||
self.assertEqual(fe.size(), 2097152)
|
||||
hashed = NativeFile("boot.bin", "dc/boot.bin", "dc",
|
||||
platform={"size": 2097152, "md5": A},
|
||||
truth={"size": 480, "md5": B})
|
||||
self.assertEqual(hashed.size(), 480)
|
||||
|
||||
def test_second_core_entry_for_a_declared_file_adds_nothing(self):
|
||||
scraped = {"systems": {"psx": {"files": [
|
||||
{"name": "scph101.bin", "destination": "bios/scph101.bin", "md5": A},
|
||||
]}}}
|
||||
truth = {"systems": {"psx": {"files": [
|
||||
{"name": "scph101.bin", "path": "scph101.bin", "md5": A},
|
||||
{"name": "scph101.bin", "path": "scph101.bin", "md5": B},
|
||||
]}}}
|
||||
systems, report = build_native_model(truth, scraped)
|
||||
self.assertEqual(report.files_added, 0)
|
||||
self.assertEqual(len(systems["psx"].files), 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -131,6 +131,106 @@ class AbsentFileIsNotReplacedByAHomonym(unittest.TestCase):
|
||||
self.assertEqual(self._resolve(), (None, "not_found"))
|
||||
|
||||
|
||||
class SecondPassKeepsIdentity(unittest.TestCase):
|
||||
"""A copy of a covered file at another core's path keeps the entry's proof.
|
||||
|
||||
The second extras pass copied a name the baseline already covers to the
|
||||
profile's own path, without its hashes or its `unsourceable:` flag, so
|
||||
the copy resolved on the name: RetroBat's pack carried Quake III's
|
||||
baseq3/pak1.pk3 as MOHAA's mainta/pak1.pk3.
|
||||
"""
|
||||
|
||||
def test_flag_and_hashes_reach_the_alternative_destination(self):
|
||||
from packextras import _collect_emulator_extras
|
||||
|
||||
sha = "a" * 40
|
||||
md5 = "b" * 32
|
||||
db = {
|
||||
"files": {sha: {"path": "bios/Q3/baseq3/pak1.pk3", "name": "pak1.pk3",
|
||||
"sha1": sha, "md5": md5, "size": 10}},
|
||||
"indexes": {"by_name": {"pak1.pk3": [sha]}, "by_md5": {md5: sha},
|
||||
"by_path_suffix": {"baseq3/pak1.pk3": [sha]}, "by_crc32": {}},
|
||||
}
|
||||
config = {
|
||||
"platform": "P", "cores": ["mohaa"], "standalone_cores": ["mohaa"],
|
||||
"systems": {"q3": {"files": [
|
||||
{"name": "pak1.pk3", "destination": "baseq3/pak1.pk3", "md5": md5}]}},
|
||||
}
|
||||
profiles = {"mohaa": {
|
||||
"emulator": "MOHAA", "type": "standalone", "cores": ["mohaa"],
|
||||
"systems": ["mohaa"],
|
||||
"files": [
|
||||
{"name": "pak1.pk3", "path": "mainta/pak1.pk3", "unsourceable": "retail"},
|
||||
{"name": "pak1.pk3", "path": "maintt/pak1.pk3", "sha1": "c" * 40},
|
||||
],
|
||||
}}
|
||||
extras = {e["destination"]: e for e in _collect_emulator_extras(
|
||||
config, "emulators", db, set(), "", profiles)}
|
||||
self.assertEqual(extras["mainta/pak1.pk3"].get("unsourceable"), "retail")
|
||||
self.assertEqual(extras["maintt/pak1.pk3"].get("sha1"), "c" * 40)
|
||||
|
||||
|
||||
class ReleaseAssetIsNotANameMatch(unittest.TestCase):
|
||||
"""The release fallback keeps the flag the local resolver honours.
|
||||
|
||||
ioquake3's baseq3/pak0.pk3 is flagged unsourceable; the local resolver
|
||||
returned not_found, then the release fallback looked the asset up by its
|
||||
bare name and handed Return to Castle Wolfenstein's demo pak to three
|
||||
platform manifests.
|
||||
"""
|
||||
|
||||
def test_an_unsourceable_entry_without_hash_never_reaches_the_asset_store(self):
|
||||
from unittest import mock
|
||||
|
||||
import packresolve
|
||||
|
||||
entry = {"name": "pak0.pk3", "unsourceable": "retail"}
|
||||
db = {"files": {}, "indexes": {"by_name": {}, "by_md5": {}, "by_path_suffix": {}}}
|
||||
with mock.patch.object(packresolve, "fetch_large_file", return_value="/x/pak0.pk3") as fetch:
|
||||
self.assertEqual(
|
||||
packresolve.resolve_file(entry, db, "bios", {}, offline=True),
|
||||
(None, "not_found"),
|
||||
)
|
||||
fetch.assert_not_called()
|
||||
|
||||
|
||||
class NameStepPrefersTheOwnersCopy(unittest.TestCase):
|
||||
"""Same-named files are ordered by where the asking profile keeps its own.
|
||||
|
||||
The name step took the first path in index order: Syobon Action's
|
||||
coin.ogg came from TheXTech's sound folder while its own copy sat in
|
||||
bios/Other/syobonaction/.
|
||||
"""
|
||||
|
||||
def test_the_copy_stored_with_the_profile_wins(self):
|
||||
import tempfile
|
||||
|
||||
from common import resolve_local_file
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
previous = os.getcwd()
|
||||
os.chdir(tmp)
|
||||
self.addCleanup(os.chdir, previous)
|
||||
other = Path("bios/Game Engines/TheXTech/sound/coin.ogg")
|
||||
own = Path("bios/Other/syobonaction/coin.ogg")
|
||||
for path, data in ((other, b"a" * 10), (own, b"b" * 12)):
|
||||
path.parent.mkdir(parents=True)
|
||||
path.write_bytes(data)
|
||||
files = {
|
||||
"1" * 40: {"path": str(other), "name": "coin.ogg", "size": 10},
|
||||
"2" * 40: {"path": str(own), "name": "coin.ogg", "size": 12},
|
||||
}
|
||||
db = {"files": files, "indexes": {
|
||||
"by_name": {"coin.ogg": ["1" * 40, "2" * 40]},
|
||||
"by_md5": {}, "by_crc32": {}, "by_path_suffix": {}}}
|
||||
entry = {"name": "coin.ogg", "path": "SyobonAction/SE/coin.ogg"}
|
||||
path, status = resolve_local_file(entry, db, dest_hint=entry["path"])
|
||||
self.assertEqual((path, status), (str(own), "name_exact"))
|
||||
entry = {"name": "coin.ogg", "path": "SE/coin.ogg", "source_profile": "syobonaction"}
|
||||
path, _status = resolve_local_file(entry, db, dest_hint=entry["path"])
|
||||
self.assertEqual(path, str(own))
|
||||
|
||||
|
||||
class CollectionCarriesNoGameDataHomonym(unittest.TestCase):
|
||||
def test_the_profiles_resolve_no_game_data_to_another_game(self):
|
||||
database = REPO_ROOT / "database.json"
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
"""Scraped system ids are slugs the profiles know.
|
||||
|
||||
The libretro scraper filed System.dat's PC-8801, DSi and CD-i under
|
||||
`nec---pc-8801`, `nintendo---nintendo-dsi` and `philips---cd-i`, fallback
|
||||
slugs no profile declares. Target filtering kept the DSi BIOS in packs for
|
||||
hardware without a DSi core, and PC-8801 survived only as a hand-added
|
||||
duplicate system carrying the quasi88 group.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(REPO_ROOT / "scripts"))
|
||||
|
||||
|
||||
class ScrapedSystemIdsAreSlugs(unittest.TestCase):
|
||||
def test_no_fallback_slug(self):
|
||||
for path in sorted((REPO_ROOT / "platforms").glob("*.yml")):
|
||||
data = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
|
||||
systems = data.get("systems") if isinstance(data, dict) else None
|
||||
for sys_id in systems or {}:
|
||||
with self.subTest(platform=path.stem, system=sys_id):
|
||||
self.assertNotIn("---", str(sys_id))
|
||||
|
||||
def test_pc88_keeps_the_mk2sr_extension_set(self):
|
||||
"""System.dat pairs the mkII SR main ROM with other models' extensions.
|
||||
|
||||
The quasi88 group supplies N88EXT0-3.ROM, which the core reads first
|
||||
(src/LIBRETRO/libretro.c:109-112) and which match that main ROM.
|
||||
"""
|
||||
from common import load_platform_config
|
||||
|
||||
config = load_platform_config("retroarch", str(REPO_ROOT / "platforms"))
|
||||
names = {
|
||||
f.get("name") for f in config["systems"]["nec-pc-88"].get("files", [])
|
||||
}
|
||||
for name in ("N88EXT0.ROM", "N88EXT1.ROM", "N88EXT2.ROM", "N88EXT3.ROM"):
|
||||
self.assertIn(name, names)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,79 @@
|
||||
"""--one-per-slot and --region read one grouping of a pack.
|
||||
|
||||
The slot pass put each core extra under every system of its profile. PicoDrive
|
||||
ranks its Mega CD BIOSes within sega-megacd; spread across sega-megadrive,
|
||||
sega-32x and the rest, they shared a tier with unranked files, the tier was
|
||||
ruled undecidable, and the keep overrode the drop.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(REPO_ROOT / "scripts"))
|
||||
|
||||
|
||||
class SlotPassReadsTheRegionGrouping(unittest.TestCase):
|
||||
def test_ranked_mega_cd_alternatives_are_decided(self):
|
||||
if not (REPO_ROOT / "database.json").is_file():
|
||||
self.skipTest("database.json not built")
|
||||
import generate_pack as gp
|
||||
from common import load_database, load_emulator_profiles, load_platform_config
|
||||
|
||||
previous = os.getcwd()
|
||||
os.chdir(REPO_ROOT)
|
||||
self.addCleanup(os.chdir, previous)
|
||||
db = load_database("database.json")
|
||||
profiles = load_emulator_profiles("emulators")
|
||||
config = load_platform_config("retroarch", "platforms")
|
||||
drops, _fallbacks, _undecided = gp._select_variants(
|
||||
config, config["systems"], "emulators", db,
|
||||
config.get("base_destination", ""), profiles, None, "full", None, True,
|
||||
)
|
||||
names = {os.path.basename(d) for d in drops}
|
||||
for name in ("jp_mcd1_9111.bin", "eu_mcd1_9210.bin"):
|
||||
self.assertIn(name, names)
|
||||
|
||||
def test_one_grouping_function(self):
|
||||
source = (REPO_ROOT / "scripts" / "generate_pack.py").read_text(encoding="utf-8")
|
||||
self.assertNotIn("def _pack_member_groups", source)
|
||||
|
||||
|
||||
class VariantGroupSpansSystems(unittest.TestCase):
|
||||
def test_us_request_withdraws_the_other_mega_cd_lists(self):
|
||||
"""PicoDrive files US under sega-segacd and EU/JP under sega-megacd.
|
||||
|
||||
find_bios (platform/libretro/libretro.c:1296) fills one slot from one
|
||||
of three lists. Grouped by system, sega-megacd had no US member and was
|
||||
kept whole as a fallback.
|
||||
"""
|
||||
if not (REPO_ROOT / "database.json").is_file():
|
||||
self.skipTest("database.json not built")
|
||||
import generate_pack as gp
|
||||
import region as region_mod
|
||||
from common import load_database, load_emulator_profiles, load_platform_config
|
||||
|
||||
previous = os.getcwd()
|
||||
os.chdir(REPO_ROOT)
|
||||
self.addCleanup(os.chdir, previous)
|
||||
db = load_database("database.json")
|
||||
profiles = load_emulator_profiles("emulators")
|
||||
config = load_platform_config("retroarch", "platforms")
|
||||
drops, _fallbacks, _undecided = gp._select_variants(
|
||||
config, config["systems"], "emulators", db,
|
||||
config.get("base_destination", ""), profiles, None, "full",
|
||||
region_mod.parse_requested("us"), False,
|
||||
)
|
||||
names = {os.path.basename(d) for d in drops}
|
||||
for name in ("jp_mcd1_9111.bin", "eu_mcd2_9306.bin"):
|
||||
self.assertIn(name, names)
|
||||
for name in ("us_scd2_9306.bin", "us_scd1_9210.bin"):
|
||||
self.assertNotIn(name, names)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -2835,12 +2835,47 @@ class TestCollectCitations(unittest.TestCase):
|
||||
citation = collect_citations(document)[0]
|
||||
self.assertEqual(citation.parts[0].path, "boot.data.c")
|
||||
|
||||
def test_external_project_prefix_is_skipped(self):
|
||||
def test_external_project_prefix_keeps_its_word(self):
|
||||
"""The project word travels with the path, the source_ref convention.
|
||||
|
||||
Dropping the run left m64plus-fz's `rice Config.cpp` unchecked
|
||||
although its declared repository vendors the plugin.
|
||||
"""
|
||||
document = {
|
||||
"notes": "ref: mt32_model.cpp:36-97, munt ROMInfo.cpp:206-213\n"
|
||||
}
|
||||
refs = [c.ref for c in collect_citations(document)]
|
||||
self.assertEqual(refs, ["mt32_model.cpp:36-97"])
|
||||
citations = collect_citations(document)
|
||||
self.assertEqual(
|
||||
[c.ref for c in citations],
|
||||
["mt32_model.cpp:36-97", "ROMInfo.cpp:206-213"],
|
||||
)
|
||||
self.assertEqual(citations[1].parts[0].path, "munt ROMInfo.cpp")
|
||||
self.assertTrue(profile_sync.is_external_citation("munt ROMInfo.cpp"))
|
||||
|
||||
def test_project_word_resolves_inside_a_declared_tree(self):
|
||||
tree = [
|
||||
"mupen64plus-video-rice/upstream/src/Config.cpp",
|
||||
"mupen64plus-video-gln64/src/Config.cpp",
|
||||
"mupen64plus-core/upstream/src/device/pif/bootrom_hle.c",
|
||||
]
|
||||
resolve = profile_sync.resolve_project_word
|
||||
self.assertEqual(
|
||||
resolve("rice Config.cpp", [tree]),
|
||||
"mupen64plus-video-rice/upstream/src/Config.cpp",
|
||||
)
|
||||
self.assertEqual(
|
||||
resolve("mupen64plus-core device/pif/bootrom_hle.c", [tree]),
|
||||
"mupen64plus-core/upstream/src/device/pif/bootrom_hle.c",
|
||||
)
|
||||
self.assertIsNone(resolve("munt ROMInfo.cpp", [tree]))
|
||||
self.assertIsNone(resolve("mupen64plus Config.cpp", [tree]))
|
||||
|
||||
def test_run_is_rendered_with_its_written_path(self):
|
||||
parts = [RefPart("mupen64plus-video-rice/upstream/src/Config.cpp", 38, 38, "38")]
|
||||
self.assertEqual(
|
||||
profile_sync._run_after_moves(parts, {0: (40, 40, None)}, "Config.cpp:38"),
|
||||
"Config.cpp:40",
|
||||
)
|
||||
|
||||
def test_linking_word_is_not_a_project(self):
|
||||
document = {"notes": "It boots, in libretro.cpp:12 as shown.\n"}
|
||||
@@ -2852,7 +2887,8 @@ class TestCollectCitations(unittest.TestCase):
|
||||
"notes": "ref: soundcanvas.cpp:55-71, 80-147, Ext plugin.cpp:23\n"
|
||||
}
|
||||
citations = collect_citations(document)
|
||||
self.assertEqual(len(citations), 1)
|
||||
self.assertEqual(len(citations), 2)
|
||||
self.assertEqual(citations[1].parts[0].path, "Ext plugin.cpp")
|
||||
self.assertEqual(citations[0].ref, "soundcanvas.cpp:55-71, 80-147")
|
||||
self.assertEqual(
|
||||
[(p.start, p.end) for p in citations[0].parts],
|
||||
@@ -3134,7 +3170,8 @@ class TestRealignProse(unittest.TestCase):
|
||||
self._git("config", "user.name", "t")
|
||||
written = PROSE_SAMPLE.replace('"pin"', '"oldpin"')
|
||||
self._commit(written, "profile")
|
||||
self._commit(PROSE_SAMPLE, "advance pin")
|
||||
# The pin advances in the working tree, the text stays committed.
|
||||
self.path.write_text(PROSE_SAMPLE, encoding="utf-8")
|
||||
|
||||
def test_run_realigns_from_the_writing_pin(self):
|
||||
self._repo_with_advanced_pin()
|
||||
@@ -3154,6 +3191,26 @@ class TestRealignProse(unittest.TestCase):
|
||||
yaml.safe_load(text)["source_commit"], "pin",
|
||||
)
|
||||
|
||||
def test_pin_moved_under_committed_text_is_not_guessed(self):
|
||||
"""A committed pin change under the same text may be a correction.
|
||||
|
||||
vitaquake2 kept its notes while 38fc9331 re-pinned it onto the
|
||||
revision those notes described; realigning from the first pin moved
|
||||
correct citations onto unrelated code.
|
||||
"""
|
||||
self._repo_with_advanced_pin()
|
||||
self._commit(PROSE_SAMPLE, "put the pin on the revision the refs describe")
|
||||
self.files[("oldpin", "a.c")] = ["x"] * 9 + ["subject"]
|
||||
self.files[("pin", "a.c")] = ["x"] * 13 + ["subject"]
|
||||
self.files[("oldpin", "b.c")] = list("pqrstuvwx")
|
||||
self.files[("pin", "b.c")] = list("pqrstuvwx")
|
||||
before = self.path.read_text()
|
||||
messages = profile_sync.realign_prose(self.path, self.tmp.name)
|
||||
self.assertTrue(
|
||||
any("pin moved under this text" in m for m in messages), messages
|
||||
)
|
||||
self.assertEqual(self.path.read_text(), before)
|
||||
|
||||
def test_dry_run_writes_nothing(self):
|
||||
self._repo_with_advanced_pin()
|
||||
self.files[("oldpin", "a.c")] = ["x"] * 9 + ["subject"]
|
||||
@@ -3192,7 +3249,7 @@ class TestRealignProse(unittest.TestCase):
|
||||
self._git("config", "user.name", "t")
|
||||
base = PROSE_SAMPLE.replace("(a.c:10)", "(deep.c:10)")
|
||||
self._commit(base.replace('"pin"', '"oldpin"'), "profile")
|
||||
self._commit(base, "advance pin")
|
||||
self.path.write_text(base, encoding="utf-8")
|
||||
profile_sync.upstream.list_tree = (
|
||||
lambda repo, sha, cache_dir, offline=False: ([], True)
|
||||
)
|
||||
@@ -3210,7 +3267,7 @@ class TestRealignProse(unittest.TestCase):
|
||||
self._git("config", "user.name", "t")
|
||||
base = PROSE_SAMPLE.replace("(a.c:10)", "(deep.c:10)")
|
||||
self._commit(base.replace('"pin"', '"oldpin"'), "profile")
|
||||
self._commit(base, "advance pin")
|
||||
self.path.write_text(base, encoding="utf-8")
|
||||
self.files[("oldpin", "src/deep.c")] = ["x"]
|
||||
self.files[("oldpin", "contrib/deep.c")] = ["y"]
|
||||
self.files[("oldpin", "b.c")] = list("pqrstuvwx")
|
||||
@@ -3226,7 +3283,7 @@ class TestRealignProse(unittest.TestCase):
|
||||
self._git("config", "user.name", "t")
|
||||
base = PROSE_SAMPLE.replace("(a.c:10)", "(deep.c:10)")
|
||||
self._commit(base.replace('"pin"', '"oldpin"'), "profile")
|
||||
self._commit(base, "advance pin")
|
||||
self.path.write_text(base, encoding="utf-8")
|
||||
self.files[("oldpin", "src/deep.c")] = ["x"] * 9 + ["subject"]
|
||||
self.files[("pin", "src/deep.c")] = ["x"] * 13 + ["subject"]
|
||||
self.files[("oldpin", "b.c")] = list("pqrstuvwx")
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
"""Data directory refreshes that other sessions can run at the same time.
|
||||
|
||||
Two refreshes of one key shared a fixed `<key>.previous` name and a
|
||||
non-atomic existence test, so the second tree landed inside the first as
|
||||
`extract/`. Staging in /tmp turned the swap into a 228 MB copy, and the
|
||||
version file was truncated in place, where a concurrent reader died on an
|
||||
empty JSON document.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import json
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
import unittest
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(REPO_ROOT / "scripts"))
|
||||
|
||||
import refresh_data_dirs as rdd # noqa: E402
|
||||
|
||||
|
||||
def _zip_bytes(files: dict[str, bytes]) -> bytes:
|
||||
buffer = io.BytesIO()
|
||||
with zipfile.ZipFile(buffer, "w") as zf:
|
||||
for name, data in files.items():
|
||||
zf.writestr(name, data)
|
||||
return buffer.getvalue()
|
||||
|
||||
|
||||
class _Response(io.BytesIO):
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *exc):
|
||||
return False
|
||||
|
||||
|
||||
class RefreshConcurrency(unittest.TestCase):
|
||||
def setUp(self):
|
||||
(REPO_ROOT / "tmp").mkdir(exist_ok=True)
|
||||
self.tmp = tempfile.TemporaryDirectory(dir=REPO_ROOT / "tmp")
|
||||
self.addCleanup(self.tmp.cleanup)
|
||||
self.root = Path(self.tmp.name)
|
||||
self.cache = self.root / "data" / "pak"
|
||||
self.versions = str(self.root / "data" / ".versions.json")
|
||||
|
||||
def test_tree_is_swapped_in_by_rename(self):
|
||||
payload = _zip_bytes({"a.bin": b"new"})
|
||||
self.cache.mkdir(parents=True)
|
||||
(self.cache / "a.bin").write_bytes(b"old")
|
||||
with mock.patch.object(
|
||||
rdd.urllib.request, "urlopen", lambda *a, **k: _Response(payload)
|
||||
), mock.patch.object(
|
||||
rdd.shutil, "move", side_effect=AssertionError("copy across filesystems")
|
||||
):
|
||||
count = rdd._download_and_extract_zip("https://x/pak.zip", str(self.cache))
|
||||
self.assertEqual(count, 1)
|
||||
self.assertEqual((self.cache / "a.bin").read_bytes(), b"new")
|
||||
leftovers = [p.name for p in self.cache.parent.iterdir() if p.name != "pak"]
|
||||
self.assertEqual(leftovers, [])
|
||||
|
||||
def test_refresh_holds_the_directory_lock(self):
|
||||
import fcntl
|
||||
|
||||
payload = _zip_bytes({"a.bin": b"x"})
|
||||
lock_path = self.cache.with_name(".pak.lock")
|
||||
seen: list[bool] = []
|
||||
|
||||
def fake_urlopen(*a, **k):
|
||||
with open(lock_path, "w") as handle:
|
||||
try:
|
||||
fcntl.flock(handle, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
except BlockingIOError:
|
||||
seen.append(True)
|
||||
else:
|
||||
fcntl.flock(handle, fcntl.LOCK_UN)
|
||||
seen.append(False)
|
||||
return _Response(payload)
|
||||
|
||||
entry = {"source_type": "zip", "source_url": "https://x/pak.zip",
|
||||
"local_cache": str(self.cache)}
|
||||
with mock.patch.object(rdd.urllib.request, "urlopen", fake_urlopen), \
|
||||
mock.patch.object(rdd, "_get_remote_etag", return_value="e1"):
|
||||
self.assertTrue(
|
||||
rdd.refresh_entry("pak", entry, force=True, versions_path=self.versions)
|
||||
)
|
||||
self.assertEqual(seen, [True])
|
||||
|
||||
def test_version_file_is_never_left_truncated(self):
|
||||
Path(self.versions).parent.mkdir(parents=True)
|
||||
Path(self.versions).write_text(json.dumps({"keep": {"sha": "1"}}))
|
||||
with mock.patch.object(rdd.json, "dump", side_effect=OSError("disk full")):
|
||||
with self.assertRaises(OSError):
|
||||
rdd._save_versions({"keep": {"sha": "2"}}, self.versions)
|
||||
self.assertEqual(json.loads(Path(self.versions).read_text()), {"keep": {"sha": "1"}})
|
||||
|
||||
def test_concurrent_writers_keep_each_other(self):
|
||||
keys = [f"k{i}" for i in range(16)]
|
||||
threads = [
|
||||
threading.Thread(
|
||||
target=rdd._record_version, args=(k, {"sha": k}, self.versions)
|
||||
)
|
||||
for k in keys
|
||||
]
|
||||
for thread in threads:
|
||||
thread.start()
|
||||
for thread in threads:
|
||||
thread.join()
|
||||
self.assertEqual(sorted(rdd._load_versions(self.versions)), sorted(keys))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
+58
-11
@@ -551,18 +551,65 @@ class TestReportAndBuilderNarrowTogether(unittest.TestCase):
|
||||
tmp.cleanup()
|
||||
|
||||
def test_one_grouping_pass_serves_both_sides(self):
|
||||
"""A second hand-rolled grouping is how the two drifted apart."""
|
||||
"""A second hand-rolled grouping is how the two drifted apart.
|
||||
|
||||
Every module is read, not only the two that once held a copy: the
|
||||
builder was split into several modules and a grouping can move with
|
||||
it. packextras.py holds the two shared passes, platform and emulator.
|
||||
"""
|
||||
scripts = Path(__file__).resolve().parent.parent / "scripts"
|
||||
hand_rolled = 0
|
||||
for name in ("generate_pack.py", "verify.py"):
|
||||
for line in (scripts / name).read_text().splitlines():
|
||||
if "region_groups.setdefault(" in line:
|
||||
hand_rolled += 1
|
||||
self.assertLessEqual(
|
||||
hand_rolled, 2,
|
||||
"platform region grouping belongs to platform_region_groups; "
|
||||
"the only other pass is the per-emulator pack shape",
|
||||
)
|
||||
for path in sorted(scripts.rglob("*.py")):
|
||||
if path.name == "packextras.py":
|
||||
continue
|
||||
with self.subTest(module=path.name):
|
||||
self.assertNotIn(
|
||||
"region_groups.setdefault(", path.read_text(encoding="utf-8"),
|
||||
"region grouping belongs to packextras: platform_region_groups "
|
||||
"or emulator_region_drops",
|
||||
)
|
||||
|
||||
def test_emulator_report_keeps_what_the_emulator_pack_keeps(self):
|
||||
"""O2EM: --region us left the Videopac BIOSes in the pack, not in the report."""
|
||||
repo = Path(__file__).resolve().parent.parent
|
||||
if not (repo / "emulators" / "o2em.yml").is_file():
|
||||
self.skipTest("no o2em profile")
|
||||
from verify import verify_emulator
|
||||
from common import load_database
|
||||
|
||||
db = load_database(str(repo / "database.json"))
|
||||
full = verify_emulator(["o2em"], str(repo / "emulators"), db)
|
||||
narrowed = verify_emulator(["o2em"], str(repo / "emulators"), db, regions=["north-america"])
|
||||
names = {d["name"] for d in narrowed["details"]}
|
||||
for kept in ("c52.bin", "g7400.bin", "jopac.bin"):
|
||||
self.assertIn(kept, names, "a system with no candidate in the region falls back")
|
||||
self.assertEqual(full["total_files"], narrowed["total_files"])
|
||||
|
||||
def test_every_grouping_call_carries_the_target(self):
|
||||
"""Integrity grouped every core's extras while the builder grouped the target's.
|
||||
|
||||
Under --target with --region, a fallback the pack kept was withdrawn
|
||||
from the check: 14 Batocera rk3568 files were never verified.
|
||||
"""
|
||||
import ast
|
||||
|
||||
scripts = Path(__file__).resolve().parent.parent / "scripts"
|
||||
for path in sorted(scripts.rglob("*.py")):
|
||||
tree = ast.parse(path.read_text(encoding="utf-8"))
|
||||
for func in ast.walk(tree):
|
||||
if not isinstance(func, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
||||
continue
|
||||
params = {a.arg for a in func.args.args + func.args.kwonlyargs}
|
||||
if "target_cores" not in params:
|
||||
continue
|
||||
for call in ast.walk(func):
|
||||
if (
|
||||
isinstance(call, ast.Call)
|
||||
and getattr(call.func, "id", None) == "platform_region_groups"
|
||||
):
|
||||
with self.subTest(module=path.name, function=func.name):
|
||||
self.assertIn(
|
||||
"target_cores", {k.arg for k in call.keywords}
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
+92
-7
@@ -168,6 +168,20 @@ class TestConflicts(unittest.TestCase):
|
||||
profile = {"dolphin": {"files": [{"name": "IPL.bin", "path": "slot/IPL.bin"}]}}
|
||||
self.assertEqual(slots.find_conflicts(config, profile, REGIONS_DB), [])
|
||||
|
||||
def test_a_bare_sibling_met_first_does_not_hide_the_contradiction(self):
|
||||
"""RetroArch declares dsi_bios7.bin bare under one system and with a
|
||||
hash under another; the pack ships the hashed one, so the slot is
|
||||
judged on it, whichever system is read first."""
|
||||
config = {
|
||||
"systems": {
|
||||
"a-console": {"files": [{"name": "IPL.bin", "destination": "GC/JAP/IPL.bin"}]},
|
||||
"b-console": {"files": [
|
||||
{"name": "IPL.bin", "destination": "GC/JAP/IPL.bin", "md5": "m" * 32}]},
|
||||
}
|
||||
}
|
||||
conflicts = slots.find_conflicts(config, self._profile("GC/JAP/IPL.bin"), REGIONS_DB)
|
||||
self.assertEqual([c.destination for c in conflicts], ["GC/JAP/IPL.bin"])
|
||||
|
||||
def test_launchers_and_aliases_never_claim(self):
|
||||
profile = {
|
||||
"launcher": {
|
||||
@@ -203,12 +217,25 @@ class TestConflicts(unittest.TestCase):
|
||||
self.assertIn("zip_contents", parameters)
|
||||
self.assertIn("data_dir_registry", parameters)
|
||||
|
||||
# And both consumers hand theirs over rather than letting it default.
|
||||
for module in (generate_pack, verify):
|
||||
parameters = inspect.signature(slots.pack_overrides).parameters
|
||||
self.assertIn("zip_contents", parameters)
|
||||
self.assertIn("data_dir_registry", parameters)
|
||||
|
||||
# Every consumer reads the one decision and hands its indexes over:
|
||||
# the ZIP builder, the install manifest and the report. The manifest
|
||||
# deciding nothing gave the installer other bytes than the ZIP.
|
||||
for module, calls in ((generate_pack, 2), (verify, 1)):
|
||||
source = inspect.getsource(module)
|
||||
call = source[source.index("slots.find_conflicts(") :][:400]
|
||||
with self.subTest(module=module.__name__):
|
||||
self.assertIn("zip_contents", call)
|
||||
self.assertNotIn("slots.arbitrate(", source)
|
||||
self.assertNotIn("slots.find_conflicts(", source)
|
||||
self.assertEqual(source.count("slots.pack_overrides("), calls)
|
||||
for start in range(len(source)):
|
||||
start = source.find("slots.pack_overrides(", start)
|
||||
if start < 0:
|
||||
break
|
||||
self.assertIn("zip_contents", source[start:start + 200])
|
||||
start += 1
|
||||
|
||||
def test_a_rom_inside_a_romset_claims_nothing_of_its_own(self):
|
||||
"""The archive occupies the destination, not the ROM it holds.
|
||||
@@ -299,9 +326,12 @@ class TestBuilderAndVerifierAgree(unittest.TestCase):
|
||||
verifier = Path(__file__).resolve().parents[1] / "scripts" / "verify.py"
|
||||
for source in (builder, verifier):
|
||||
text = source.read_text(encoding="utf-8")
|
||||
self.assertIn("slots.find_conflicts(", text, source.name)
|
||||
self.assertIn("slots.arbitrate(", text, source.name)
|
||||
self.assertIn("decision.serves_both", text, source.name)
|
||||
self.assertIn("slots.pack_overrides(", text, source.name)
|
||||
self.assertNotIn("decision.serves_both", text, source.name)
|
||||
self.assertIn(
|
||||
"decision.serves_both",
|
||||
(builder.parent / "slots.py").read_text(encoding="utf-8"),
|
||||
)
|
||||
|
||||
def test_neither_reimplements_the_mode_test(self):
|
||||
# A local "if mode == md5" beside the override would drift from the
|
||||
@@ -339,6 +369,17 @@ class TestSelfContradictingDestinations(unittest.TestCase):
|
||||
)
|
||||
self.assertEqual(slots.find_collisions(config, REGIONS_DB), [])
|
||||
|
||||
def test_a_file_both_declarations_accept_settles_the_path(self):
|
||||
"""RetroDECK: ATARIOSB.ROM under one system with one md5, under
|
||||
another with a list naming that same md5; the shipped file passes
|
||||
both, so --strict has nothing to correct there."""
|
||||
config = self._config(
|
||||
{"name": "IPL.bin", "destination": "disk.rom", "md5": "m" * 32},
|
||||
{"name": "IPL.bin", "destination": "disk.rom",
|
||||
"md5": f"{'n' * 32},{'m' * 32}"},
|
||||
)
|
||||
self.assertEqual(slots.find_collisions(config, REGIONS_DB), [])
|
||||
|
||||
def test_a_primary_and_its_pinned_variant_are_one_family(self):
|
||||
self.assertTrue(
|
||||
slots._same_file_family(
|
||||
@@ -470,5 +511,49 @@ class TestProvenEvidence(unittest.TestCase):
|
||||
)
|
||||
|
||||
|
||||
|
||||
class ManifestFollowsTheArbitration(unittest.TestCase):
|
||||
"""The installer is handed the file the ZIP carries on a contested slot."""
|
||||
|
||||
def test_retroarch_manifest_serves_every_override(self):
|
||||
repo = Path(__file__).resolve().parents[1]
|
||||
if not (repo / "database.json").is_file():
|
||||
self.skipTest("no database.json")
|
||||
# The database names files relative to the repository root, and this
|
||||
# module's fixtures move the working directory elsewhere.
|
||||
previous = os.getcwd()
|
||||
os.chdir(repo)
|
||||
self.addCleanup(os.chdir, previous)
|
||||
import generate_pack
|
||||
from common import (
|
||||
build_zip_contents_index,
|
||||
load_data_dir_registry,
|
||||
load_database,
|
||||
load_emulator_profiles,
|
||||
load_platform_config,
|
||||
)
|
||||
|
||||
db = load_database(str(repo / "database.json"))
|
||||
config = load_platform_config("retroarch", str(repo / "platforms"))
|
||||
profiles = load_emulator_profiles(str(repo / "emulators"))
|
||||
overrides = slots.pack_overrides(
|
||||
config, profiles, db, build_zip_contents_index(db),
|
||||
load_data_dir_registry(str(repo / "platforms")),
|
||||
)
|
||||
if not overrides:
|
||||
self.skipTest("no contested slot the pack settles on its own")
|
||||
manifest = generate_pack.generate_manifest(
|
||||
"retroarch", str(repo / "platforms"), db, str(repo / "bios"),
|
||||
str(repo / "platforms" / "_registry.yml"),
|
||||
emulators_dir=str(repo / "emulators"), emu_profiles=profiles, offline=True,
|
||||
)
|
||||
base = config.get("base_destination", "")
|
||||
by_dest = {f["dest"]: f for f in manifest["files"]}
|
||||
for destination, path in overrides.items():
|
||||
dest = destination[len(base) + 1:] if base else destination
|
||||
with self.subTest(destination=destination):
|
||||
self.assertEqual(by_dest[dest]["repo_path"], path)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,89 @@
|
||||
"""One rule picks the held file that satisfies the emulator.
|
||||
|
||||
verify credited a variant found through the emulator's hashes while the
|
||||
platform pack only searched by name and kept the platform's md5: the report
|
||||
called ATARIOSB.ROM satisfied while the pack shipped the dump atari800
|
||||
rejects, and `verify --emulator azahar` credited an otp.bin the emulator pack
|
||||
never swapped in. Both sides now read find_validated_variant.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import re
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(REPO_ROOT / "scripts"))
|
||||
|
||||
from validation import find_validated_variant # noqa: E402
|
||||
|
||||
|
||||
class FindValidatedVariant(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self._tmp = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self._tmp.cleanup)
|
||||
root = Path(self._tmp.name)
|
||||
self.bad = root / "rom.bin"
|
||||
self.bad.write_bytes(b"x" * 8)
|
||||
self.good = root / "other_name.bin"
|
||||
self.good.write_bytes(b"y" * 16)
|
||||
self.alias = root / "v" / "rom.bin"
|
||||
self.alias.parent.mkdir()
|
||||
self.alias.write_bytes(b"z" * 16)
|
||||
|
||||
def entry(path: Path) -> tuple[str, dict]:
|
||||
data = path.read_bytes()
|
||||
sha1 = hashlib.sha1(data).hexdigest()
|
||||
return sha1, {"path": str(path), "name": path.name, "sha1": sha1,
|
||||
"md5": hashlib.md5(data).hexdigest(), "size": len(data)}
|
||||
|
||||
self.files = dict(entry(p) for p in (self.bad, self.good, self.alias))
|
||||
self.good_sha1 = hashlib.sha1(self.good.read_bytes()).hexdigest()
|
||||
self.db = {"files": self.files, "indexes": {
|
||||
"by_name": {"rom.bin": [s for s, e in self.files.items() if e["name"] == "rom.bin"]},
|
||||
"by_md5": {e["md5"]: s for s, e in self.files.items()},
|
||||
}}
|
||||
from validation import _build_validation_index
|
||||
|
||||
self.by_hash = _build_validation_index({"emu": {"files": [
|
||||
{"name": "rom.bin", "validation": ["size", "sha1"], "size": 16,
|
||||
"sha1": self.good_sha1}]}})
|
||||
self.by_size = _build_validation_index({"emu": {"files": [
|
||||
{"name": "rom.bin", "validation": ["size"], "size": 16}]}})
|
||||
|
||||
def test_a_dump_held_under_another_name_is_found_by_the_emulator_hash(self):
|
||||
found = find_validated_variant({"name": "rom.bin"}, self.db, str(self.bad), self.by_hash)
|
||||
self.assertEqual(found, str(self.good))
|
||||
|
||||
def test_a_digest_platform_only_takes_a_value_its_entry_declares(self):
|
||||
alias_md5 = hashlib.md5(self.alias.read_bytes()).hexdigest()
|
||||
entry = {"name": "rom.bin", "md5": alias_md5}
|
||||
found = find_validated_variant(
|
||||
entry, self.db, str(self.bad), self.by_size, platform_digest="md5")
|
||||
self.assertEqual(found, str(self.alias))
|
||||
entry = {"name": "rom.bin", "md5": "0" * 32}
|
||||
self.assertIsNone(find_validated_variant(
|
||||
entry, self.db, str(self.bad), self.by_size, platform_digest="md5"))
|
||||
|
||||
|
||||
class OneImplementation(unittest.TestCase):
|
||||
"""The report and the builders call the same finder and keep no copy."""
|
||||
|
||||
def test_no_module_carries_its_own_variant_search(self):
|
||||
for name in ("verify.py", "generate_pack.py", "packresolve.py"):
|
||||
source = (REPO_ROOT / "scripts" / name).read_text(encoding="utf-8")
|
||||
with self.subTest(module=name):
|
||||
self.assertNotRegex(source, r"def _find_best_variant|def _find_candidate_satisfying_both")
|
||||
for name in ("verify.py", "generate_pack.py"):
|
||||
source = (REPO_ROOT / "scripts" / name).read_text(encoding="utf-8")
|
||||
with self.subTest(module=name):
|
||||
self.assertGreaterEqual(len(re.findall(r"find_validated_variant\(", source)), 2
|
||||
if name == "generate_pack.py" else 3)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,138 @@
|
||||
"""--verify-packs opens the pack the flags name, by its exact file name.
|
||||
|
||||
The check took the first sorted *_BIOS_Pack.zip whose name contained the
|
||||
platform name. With a full and a regional pack side by side, --region us
|
||||
judged the full pack against the regional expectation and never opened the
|
||||
regional one; a pack built with --source truth answered SKIP and exit 0.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(REPO_ROOT / "scripts"))
|
||||
|
||||
|
||||
def _ok_result() -> tuple:
|
||||
return (True, 0, 0, [], 1, 1, 1, 1, 0)
|
||||
|
||||
|
||||
class VerifyPackChoice(unittest.TestCase):
|
||||
def setUp(self):
|
||||
import generate_pack
|
||||
|
||||
self.gp = generate_pack
|
||||
previous = os.getcwd()
|
||||
os.chdir(REPO_ROOT)
|
||||
self.addCleanup(os.chdir, previous)
|
||||
self.tmp = tempfile.TemporaryDirectory(dir=REPO_ROOT / "tmp")
|
||||
self.addCleanup(self.tmp.cleanup)
|
||||
self.out = Path(self.tmp.name)
|
||||
self.db = self.out / "db.json"
|
||||
self.db.write_text(json.dumps({"files": {}}))
|
||||
self.stem = generate_pack._platform_pack_stem(
|
||||
["retroarch", "lakka"], "retroarch", "platforms"
|
||||
)
|
||||
|
||||
def _args(self, **overrides) -> argparse.Namespace:
|
||||
values = {
|
||||
"db": str(self.db),
|
||||
"platform": "retroarch",
|
||||
"all": False,
|
||||
"include_archived": False,
|
||||
"emulators_dir": "emulators",
|
||||
"platforms_dir": "platforms",
|
||||
"output_dir": str(self.out),
|
||||
"target": None,
|
||||
"regions": [],
|
||||
}
|
||||
values.update(overrides)
|
||||
return argparse.Namespace(**values)
|
||||
|
||||
def _run(self, args) -> list[str]:
|
||||
checked: list[str] = []
|
||||
|
||||
def record(zip_path, platform_name, *a, **kw):
|
||||
checked.append((platform_name, os.path.basename(zip_path)))
|
||||
return _ok_result()
|
||||
|
||||
with mock.patch.object(self.gp, "verify_pack_against_platform", record):
|
||||
self.gp._run_verify_packs(args)
|
||||
return checked
|
||||
|
||||
def _touch(self, name: str) -> None:
|
||||
(self.out / name).write_bytes(b"")
|
||||
|
||||
def test_region_opens_the_regional_pack(self):
|
||||
import region as region_mod
|
||||
|
||||
regions = region_mod.parse_requested("us")
|
||||
tag = self.gp._narrowings("full", regions, None, False, False)[0][0]
|
||||
self._touch(f"{self.stem}_BIOS_Pack.zip")
|
||||
self._touch(f"{self.stem}{tag}_BIOS_Pack.zip")
|
||||
checked = self._run(self._args(regions=regions))
|
||||
self.assertEqual(checked, [("retroarch", f"{self.stem}{tag}_BIOS_Pack.zip")])
|
||||
|
||||
def test_unnarrowed_check_ignores_the_regional_pack(self):
|
||||
self._touch(f"{self.stem}_AsiaPacific_BIOS_Pack.zip")
|
||||
self._touch(f"{self.stem}_BIOS_Pack.zip")
|
||||
checked = self._run(self._args())
|
||||
self.assertEqual(checked, [("retroarch", f"{self.stem}_BIOS_Pack.zip")])
|
||||
|
||||
def test_named_platform_with_only_a_narrowed_pack_fails(self):
|
||||
self._touch(f"{self.stem}_Truth_BIOS_Pack.zip")
|
||||
with self.assertRaises(SystemExit) as ctx:
|
||||
self._run(self._args())
|
||||
self.assertEqual(ctx.exception.code, 1)
|
||||
|
||||
def test_include_archived_reaches_the_archived_pack(self):
|
||||
stem = self.gp._platform_pack_stem(["retropie"], "retropie", "platforms")
|
||||
self._touch(f"{stem}_BIOS_Pack.zip")
|
||||
checked = self._run(
|
||||
self._args(platform=None, all=True, include_archived=True)
|
||||
)
|
||||
self.assertIn(("retropie", f"{stem}_BIOS_Pack.zip"), checked)
|
||||
|
||||
|
||||
class GroupRenameKeepsSystemTag(unittest.TestCase):
|
||||
def test_single_system_pack_does_not_take_the_full_name(self):
|
||||
import generate_pack as gp
|
||||
|
||||
previous = os.getcwd()
|
||||
os.chdir(REPO_ROOT)
|
||||
self.addCleanup(os.chdir, previous)
|
||||
with tempfile.TemporaryDirectory(dir=REPO_ROOT / "tmp") as tmp:
|
||||
built = Path(tmp) / f"RetroArch_x_BIOS_Pack{gp._system_tag(['atari-lynx'])}.zip"
|
||||
|
||||
def fake_generate(*a, **kw):
|
||||
built.write_bytes(b"")
|
||||
return str(built)
|
||||
|
||||
args = argparse.Namespace(
|
||||
all_variants=False, source="full", required_only=False,
|
||||
platforms_dir="platforms", target=None, split=False,
|
||||
include_extras=False, emulators_dir="emulators", regions=[],
|
||||
one_per_slot=False, offline=True, bios_dir="bios",
|
||||
output_dir=tmp,
|
||||
)
|
||||
with mock.patch.object(gp, "generate_pack", fake_generate), \
|
||||
mock.patch.object(gp, "verify_and_finalize_packs", return_value=True):
|
||||
gp._run_platform_packs(
|
||||
args, [(["retroarch", "lakka"], "retroarch")], {}, {}, {},
|
||||
{}, {}, ["atari-lynx"],
|
||||
)
|
||||
names = os.listdir(tmp)
|
||||
stem = gp._platform_pack_stem(["retroarch", "lakka"], "retroarch", "platforms")
|
||||
self.assertEqual(names, [f"{stem}_BIOS_Pack_Lynx.zip"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user