Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,20 @@ recomp_target_launcher_ui(recomp-ui-launcher CONSOLE all)

include(CTest)
if(BUILD_TESTING)
add_executable(recomp-ui-ips-patch-test
tests/ips_patch_test.c
src/common/ips_patch.c
src/common/crc32.c)
target_include_directories(recomp-ui-ips-patch-test PRIVATE src/common)
add_test(NAME recomp-ui-ips-patch COMMAND $<TARGET_FILE:recomp-ui-ips-patch-test>)

add_executable(recomp-ui-rom-patch-tool
tools/rom_patch_tool.c
src/common/ips_patch.c
src/common/crc32.c
src/common/sha1.c)
target_include_directories(recomp-ui-rom-patch-tool PRIVATE src/common)

add_test(
NAME recomp-ui-asset-manifests
COMMAND ${CMAKE_COMMAND}
Expand Down
73 changes: 68 additions & 5 deletions src/common/backends/imgui/launcher_imgui.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3260,6 +3260,18 @@ void draw_assist_tools(LauncherModel* m, const LauncherTheme& th) {
"features. The game window should disclose when it is active.");
ImGui::PopTextWrapPos();
ImGui::PopStyleColor();
ImGui::Dummy(ImVec2(0, px(8)));
ImGui::PushStyleColor(ImGuiCol_Text, col(th.accent2));
ImGui::TextUnformatted("FAST-FORWARD SPEED");
ImGui::PopStyleColor();
ImGui::TextColored(col(th.text_muted),
"Target emulation speed while Fast-forward is held or latched.");
int speed = m->s.assist_fast_forward_multiplier;
ImGui::SetNextItemWidth(px(360));
if (ImGui::SliderInt("##assist_fast_forward_speed", &speed,
m->assist_fast_forward_min,
m->assist_fast_forward_max, "%dx"))
m->s.assist_fast_forward_multiplier = speed;
if (m->settings_bindings) {
ImGui::Dummy(ImVec2(0, px(8)));
draw_assist_binding_editor(m, th, "assist_binds", 0, true);
Expand Down Expand Up @@ -5657,7 +5669,7 @@ static void mod_note_error(LauncherModel* m) {

static bool mod_commit_launch(LauncherModel* m) {
if (!m || !m->mods || !m->mods->commit ||
m->mods->commit(m->mods->ctx, launcher_model_rom_path(m))) {
m->mods->commit(m->mods->ctx, launcher_model_effective_rom_path(m))) {
return true;
}
mod_note_error(m);
Expand Down Expand Up @@ -6488,9 +6500,58 @@ static void draw_mod_features(LauncherModel* m, const LauncherTheme& th) {
ImGui::EndChild();
}

static void draw_rom_patch(LauncherModel* m, const LauncherTheme& th) {
if (!m || !m->rom_patch_supported) return;

if (ImGui::BeginChild("##rom_patch", ImVec2(0, px(170)),
ImGuiChildFlags_Borders)) {
eyebrow("ROM PATCH");
bool enabled = m->s.rom_patch_enabled != 0;
if (ImGui::Checkbox("Enable ROM patch", &enabled))
launcher_model_toggle_rom_patch(m);

const float clear_w = px(70);
const float browse_w = px(95);
ImGui::SameLine(ImGui::GetContentRegionAvail().x - browse_w - clear_w -
px(8));
if (ImGui::Button("Browse...", ImVec2(browse_w, px(28)))) {
static const char* patterns[] = { "*.ips", "*.ips32", "*.bps" };
if (launcher_pick_file("Select ROM patch", patterns, 3,
"ROM patches", g_pick_buf,
sizeof(g_pick_buf))) {
launcher_model_set_rom_patch(m, g_pick_buf);
}
}
ImGui::SameLine(0, px(8));
ImGui::BeginDisabled(!m->s.rom_patch_path[0]);
if (ImGui::Button("Clear", ImVec2(clear_w, px(28))))
launcher_model_clear_rom_patch(m);
ImGui::EndDisabled();

const char* path = m->s.rom_patch_path[0]
? m->s.rom_patch_path : "No patch selected";
char elided[384];
elide_left(path, ImGui::GetContentRegionAvail().x,
elided, sizeof(elided));
ImGui::TextColored(col(th.text_muted), "%s", elided);
if (ImGui::IsItemHovered() && m->s.rom_patch_path[0])
ImGui::SetTooltip("%s", m->s.rom_patch_path);
if (m->rom_patch_note && m->rom_patch_note[0])
ImGui::TextWrapped("%s", m->rom_patch_note);
if (m->rom_patch_status[0])
ImGui::TextColored(col(th.accent), "%s", m->rom_patch_status);
}
ImGui::EndChild();
}

void draw_mods(LauncherModel* m, const LauncherTheme& th) {
const auto* mods = m ? m->mods : nullptr;
if (!mods) return;
if (!m || (!mods && !m->rom_patch_supported)) return;
if (m->rom_patch_supported) {
draw_rom_patch(m, th);
if (!mods) return;
ImGui::Dummy(ImVec2(0, px(8)));
}
const bool feature_provider =
mods->feature_count && mods->feature_get &&
mods->feature_option_get && mods->feature_enable &&
Expand Down Expand Up @@ -6691,8 +6752,10 @@ void draw_footer(LauncherModel* m, const LauncherTheme& th, float footer_h) {
/* Prefer mismatch prompt over launch even if can_play races true. */
if (bios_block)
launcher_model_bios_play_prompt(m);
else if (mod_commit_launch(m))
else if (launcher_model_prepare_rom_patch(m) && mod_commit_launch(m))
m->action = LNG_ACTION_LAUNCH;
else if (m->rom_patch_supported && m->s.rom_patch_enabled)
launcher_model_set_view(m, LNG_VIEW_MODS);
} else if (!play_enabled && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
const char* noun = m->rom_noun ? m->rom_noun : "ROM";
if (m->has_bios && !m->setup_bios_ok) {
Expand Down Expand Up @@ -7658,15 +7721,15 @@ void draw_ui(LauncherModel* m, const LauncherTheme& th, int logical_w, int logic
const float right = ImGui::GetWindowContentRegionMax().x;
const float y = hdr_top + px(6.0f);
if (m->view == LNG_VIEW_DASHBOARD) {
const int count = 1 + (m->mods ? 1 : 0) +
const int count = 1 + ((m->mods || m->rom_patch_supported) ? 1 : 0) +
(m->has_assist_tools ? 1 : 0) +
((m->credits_text && m->credits_text[0]) ? 1 : 0);
const float w = px(110.0f);
const float total = w * count + gap * (count - 1);
ImGui::SetCursorPos(ImVec2(right - total, y));
if (ImGui::Button("Settings", ImVec2(w, px(34))))
launcher_model_set_view(m, LNG_VIEW_SETTINGS);
if (m->mods) {
if (m->mods || m->rom_patch_supported) {
ImGui::SameLine(0, gap);
if (ImGui::Button("Mods", ImVec2(w, px(34))))
launcher_model_set_view(m, LNG_VIEW_MODS);
Expand Down
190 changes: 183 additions & 7 deletions src/common/ips_patch.c
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
// ips_patch.c — classic IPS patch format implementation. See ips_patch.h.

#include "ips_patch.h"
#include "crc32.h"

#include <limits.h>
#include <stdlib.h>
#include <string.h>

Expand All @@ -12,18 +14,42 @@
static bool ips_grow(uint8_t** buf, size_t* cap, size_t need) {
if (need <= *cap) return true;
size_t ncap = *cap ? *cap : 4096;
while (ncap < need) ncap *= 2;
while (ncap < need) {
if (ncap > SIZE_MAX / 2) { ncap = need; break; }
ncap *= 2;
}
uint8_t* nb = (uint8_t*)realloc(*buf, ncap);
if (!nb) return false;
*buf = nb;
*cap = ncap;
return true;
}

IpsPatchFormat ips_detect_format(const uint8_t* patch, size_t patch_len) {
if (!patch || patch_len < 4) return IPS_PATCH_UNKNOWN;
if (patch_len >= 5 && memcmp(patch, "PATCH", 5) == 0) return IPS_PATCH_CLASSIC;
if (patch_len >= 5 && memcmp(patch, "IPS32", 5) == 0) return IPS_PATCH_IPS32;
if (memcmp(patch, "BPS1", 4) == 0) return IPS_PATCH_BPS;
return IPS_PATCH_UNKNOWN;
}

static size_t ips_read_be(const uint8_t* p, size_t width) {
size_t value = 0;
for (size_t i = 0; i < width; ++i) value = (value << 8) | p[i];
return value;
}

bool ips_apply(const uint8_t* src, size_t src_len,
const uint8_t* patch, size_t patch_len,
uint8_t** out_data, size_t* out_len) {
if (!patch || patch_len < 8 || memcmp(patch, "PATCH", 5) != 0) return false;
if (!out_data || !out_len || (src_len && !src)) return false;
const IpsPatchFormat format = ips_detect_format(patch, patch_len);
if (format != IPS_PATCH_CLASSIC && format != IPS_PATCH_IPS32)
return false;
const size_t offset_width = format == IPS_PATCH_IPS32 ? 4 : 3;
const size_t footer_width = format == IPS_PATCH_IPS32 ? 4 : 3;
const char* footer = format == IPS_PATCH_IPS32 ? "EEOF" : "EOF";
if (patch_len < 5 + footer_width) return false;

size_t cap = src_len ? src_len : 4096;
uint8_t* out = (uint8_t*)malloc(cap);
Expand All @@ -32,16 +58,17 @@ bool ips_apply(const uint8_t* src, size_t src_len,
if (src && src_len) memcpy(out, src, src_len);

size_t i = 5;
while (i + 3 <= patch_len) {
if (memcmp(patch + i, "EOF", 3) == 0) {
while (i + footer_width <= patch_len) {
if (memcmp(patch + i, footer, footer_width) == 0) {
i += footer_width;
*out_data = out;
*out_len = len;
return true;
}

// 24-bit big-endian offset.
size_t off = ((size_t)patch[i] << 16) | ((size_t)patch[i + 1] << 8) | patch[i + 2];
i += 3;
if (i + offset_width > patch_len) { free(out); return false; }
const size_t off = ips_read_be(patch + i, offset_width);
i += offset_width;

if (i + 2 > patch_len) { free(out); return false; }
// 16-bit big-endian length. len==0 introduces an RLE record instead.
Expand All @@ -53,12 +80,14 @@ bool ips_apply(const uint8_t* src, size_t src_len,
size_t run = ((size_t)patch[i] << 8) | patch[i + 1];
i += 2;
uint8_t val = patch[i++];
if (run > SIZE_MAX - off) { free(out); return false; }
size_t need = off + run;
if (!ips_grow(&out, &cap, need)) { free(out); return false; }
if (need > len) { memset(out + len, 0, need - len); len = need; }
memset(out + off, val, run);
} else {
if (i + rec_len > patch_len) { free(out); return false; }
if (rec_len > SIZE_MAX - off) { free(out); return false; }
size_t need = off + rec_len;
if (!ips_grow(&out, &cap, need)) { free(out); return false; }
if (need > len) { memset(out + len, 0, need - len); len = need; }
Expand All @@ -73,3 +102,150 @@ bool ips_apply(const uint8_t* src, size_t src_len,
*out_len = len;
return true;
}

static uint32_t bps_read_le32(const uint8_t* p) {
return (uint32_t)p[0] | ((uint32_t)p[1] << 8) |
((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);
}

static bool bps_read_number(const uint8_t* patch, size_t limit,
size_t* cursor, uint64_t* out) {
uint64_t value = 0;
uint64_t shift = 1;
for (;;) {
if (*cursor >= limit) return false;
const uint8_t x = patch[(*cursor)++];
const uint64_t digit = x & 0x7fu;
if (digit && shift > (UINT64_MAX - value) / digit) return false;
value += digit * shift;
if (x & 0x80u) {
*out = value;
return true;
}
if (shift > UINT64_MAX / 128u) return false;
shift <<= 7;
if (value > UINT64_MAX - shift) return false;
value += shift;
}
}

static bool bps_move_relative(int64_t* relative, uint64_t encoded) {
const uint64_t magnitude = encoded >> 1;
if (magnitude > (uint64_t)INT64_MAX) return false;
const int64_t amount = (int64_t)magnitude;
if (encoded & 1u) {
if (*relative < INT64_MIN + amount) return false;
*relative -= amount;
} else {
if (*relative > INT64_MAX - amount) return false;
*relative += amount;
}
return true;
}

bool bps_apply(const uint8_t* src, size_t src_len,
const uint8_t* patch, size_t patch_len,
uint8_t** out_data, size_t* out_len) {
if (!out_data || !out_len || (src_len && !src) || !patch ||
patch_len < 4 + 3 + 12 || memcmp(patch, "BPS1", 4) != 0)
return false;

const size_t command_end = patch_len - 12;
const uint32_t expected_source_crc = bps_read_le32(patch + command_end);
const uint32_t expected_target_crc = bps_read_le32(patch + command_end + 4);
const uint32_t expected_patch_crc = bps_read_le32(patch + command_end + 8);
if (recompui_crc32_compute(patch, patch_len - 4) != expected_patch_crc ||
recompui_crc32_compute(src, src_len) != expected_source_crc)
return false;

size_t cursor = 4;
uint64_t source_size = 0, target_size = 0, metadata_size = 0;
if (!bps_read_number(patch, command_end, &cursor, &source_size) ||
!bps_read_number(patch, command_end, &cursor, &target_size) ||
!bps_read_number(patch, command_end, &cursor, &metadata_size) ||
source_size != src_len || target_size > SIZE_MAX ||
metadata_size > command_end - cursor)
return false;
cursor += (size_t)metadata_size;

const size_t target_len = (size_t)target_size;
uint8_t* target = (uint8_t*)malloc(target_len ? target_len : 1);
if (!target) return false;

size_t output = 0;
int64_t source_relative = 0;
int64_t target_relative = 0;
while (output < target_len) {
uint64_t command = 0;
if (!bps_read_number(patch, command_end, &cursor, &command)) {
free(target);
return false;
}
const unsigned action = (unsigned)(command & 3u);
const uint64_t encoded_length = (command >> 2) + 1;
if (encoded_length > target_len - output) {
free(target);
return false;
}
const size_t length = (size_t)encoded_length;

if (action == 0) {
if (output > src_len || length > src_len - output) {
free(target);
return false;
}
memcpy(target + output, src + output, length);
output += length;
} else if (action == 1) {
if (cursor > command_end || length > command_end - cursor) {
free(target);
return false;
}
memcpy(target + output, patch + cursor, length);
cursor += length;
output += length;
} else {
uint64_t delta = 0;
int64_t* relative = action == 2 ? &source_relative
: &target_relative;
if (!bps_read_number(patch, command_end, &cursor, &delta) ||
!bps_move_relative(relative, delta)) {
free(target);
return false;
}
for (size_t i = 0; i < length; ++i) {
if (*relative < 0) {
free(target);
return false;
}
const uint64_t index = (uint64_t)*relative;
if ((action == 2 && index >= src_len) ||
(action == 3 && index >= output)) {
free(target);
return false;
}
target[output++] = action == 2 ? src[(size_t)index]
: target[(size_t)index];
++*relative;
}
}
}

if (cursor != command_end ||
recompui_crc32_compute(target, target_len) != expected_target_crc) {
free(target);
return false;
}
*out_data = target;
*out_len = target_len;
return true;
}

bool rom_patch_apply(const uint8_t* src, size_t src_len,
const uint8_t* patch, size_t patch_len,
uint8_t** out_data, size_t* out_len) {
const IpsPatchFormat format = ips_detect_format(patch, patch_len);
if (format == IPS_PATCH_BPS)
return bps_apply(src, src_len, patch, patch_len, out_data, out_len);
return ips_apply(src, src_len, patch, patch_len, out_data, out_len);
}
Loading