summaryrefslogtreecommitdiff
path: root/Source/Core/DiscIO/RiivolutionPatcher.cpp
blob: 7269a37b6f14b844a8df67b0a3fb659438f88d2a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
// Copyright 2021 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later

#include "DiscIO/RiivolutionPatcher.h"

#include <algorithm>
#include <locale>
#include <string>
#include <string_view>
#include <vector>

#include <fmt/format.h>

#include "Common/FileUtil.h"
#include "Common/IOFile.h"
#include "Common/StringUtil.h"
#include "Core/AchievementManager.h"
#include "Core/Core.h"
#include "Core/HLE/HLE.h"
#include "Core/HW/Memmap.h"
#include "Core/IOS/FS/FileSystem.h"
#include "Core/PowerPC/MMU.h"
#include "Core/System.h"
#include "DiscIO/DirectoryBlob.h"
#include "DiscIO/RiivolutionParser.h"

namespace DiscIO::Riivolution
{
FileDataLoader::~FileDataLoader() = default;

FileDataLoaderHostFS::FileDataLoaderHostFS(std::string sd_root, const std::string& xml_path,
                                           std::string_view patch_root)
    : m_sd_root(std::move(sd_root))
{
  // Riivolution treats 'external' file paths as follows:
  // - If it starts with a '/', it's an absolute path, ie. relative to the SD card root.
  // - Otherwise:
  //   - If the 'root' parameter of the current patch is not set or is empty, the path is relative
  //     to the folder the XML file is in.
  //   - If the 'root' parameter of the current patch starts with a '/', the path is relative to
  //     that folder on the SD card, starting at the SD card root.
  //   - If the 'root' parameter of the current patch starts without a '/', the path is relative to
  //     that folder on the SD card, starting at the folder the XML file is in.
  // The following initialization should properly replicate this behavior.

  // First set m_patch_root to the folder the parsed XML file is in.
  SplitPath(xml_path, &m_patch_root, nullptr, nullptr);

  // Then try to resolve the given patch_root as if it was a file path, and on success replace the
  // m_patch_root with it.
  if (!patch_root.empty())
  {
    auto r = MakeAbsoluteFromRelative(patch_root);
    if (r)
      m_patch_root = std::move(*r);
  }
}

std::optional<std::string>
FileDataLoaderHostFS::MakeAbsoluteFromRelative(std::string_view external_relative_path)
{
#ifdef _WIN32
  // Riivolution treats a backslash as just a standard filename character, but we can't replicate
  // this properly on Windows. So if a file contains a backslash, immediately error out.
  if (external_relative_path.find("\\") != std::string_view::npos)
    return std::nullopt;
#endif

  const std::string& root = external_relative_path.starts_with('/') ? m_sd_root : m_patch_root;
  std::string result = root;
  std::string_view work = external_relative_path;

  // Strip away all leading and trailing path separators.
  while (work.starts_with('/'))
    work.remove_prefix(1);
  while (work.ends_with('/'))
    work.remove_suffix(1);
  size_t depth = 0;
  while (true)
  {
    if (work.empty())
      break;

    // Extract a single path element.
    size_t separator_position = work.find('/');
    std::string_view element = work.substr(0, separator_position);

    if (element == ".")
    {
      // This is a harmless element, doesn't change any state.
    }
    else if (element == "..")
    {
      // We're going up a level.
      // If this isn't possible someone is trying to exit the root directory, prevent that.
      if (depth == 0)
        return std::nullopt;
      --depth;

      // Remove the last path element from the result string.
      // This must have been previously attached in the branch below (otherwise depth would have
      // been 0), so there's no need to check whether the string is empty or anything like that.
      while (result.back() != '/')
        result.pop_back();
      result.pop_back();
    }
    else if (std::ranges::all_of(element, [](char c) { return c == '.'; }))
    {
      // This is a triple, quadruple, etc. dot.
      // Some file systems treat this as several 'up' path traversals, but Riivolution does not.
      // If someone tries this just error out, it wouldn't work sensibly in Riivolution anyway.
      return std::nullopt;
    }
    else
    {
      // We're going down a level.
      ++depth;

      // Append path element to result string.
      result += '/';
      result += element;

      // Riivolution assumes a case-insensitive file system, which means it's possible that an XML
      // file references a 'file.bin' but the actual file is named 'File.bin' or 'FILE.BIN'. To
      // preserve this behavior, we modify the file path to match any existing file in the file
      // system, if one exists.
      if (!::File::Exists(result))
      {
        // Drop path element again so we can search in the directory.
        result.erase(result.size() - element.size(), element.size());

        // Re-attach an element that actually matches the capitalization in the host filesystem.
        auto possible_files = ::File::ScanDirectoryTree(result, false);
        bool found = false;
        for (auto& f : possible_files.children)
        {
          if (Common::CaseInsensitiveEquals(element, f.virtualName))
          {
            result += f.virtualName;
            found = true;
            break;
          }
        }

        // If there isn't any file that matches just use the given element.
        if (!found)
          result += element;
      }
    }

    // If this was the last path element, we're done.
    if (separator_position == std::string_view::npos)
      break;

    // Remove element from work string.
    work = work.substr(separator_position + 1);

    // Remove any potential extra path separators.
    while (work.starts_with('/'))
      work = work.substr(1);
  }
  return result;
}

std::optional<u64>
FileDataLoaderHostFS::GetExternalFileSize(std::string_view external_relative_path)
{
  auto path = MakeAbsoluteFromRelative(external_relative_path);
  if (!path)
    return std::nullopt;
  ::File::FileInfo f(*path);
  if (!f.IsFile())
    return std::nullopt;
  return f.GetSize();
}

std::vector<u8> FileDataLoaderHostFS::GetFileContents(std::string_view external_relative_path)
{
  auto path = MakeAbsoluteFromRelative(external_relative_path);
  if (!path)
    return {};
  ::File::IOFile f(*path, "rb");
  if (!f)
    return {};
  const u64 length = f.GetSize();
  std::vector<u8> value;
  value.resize(length);
  if (!f.ReadBytes(value.data(), length))
    return {};
  return value;
}

std::vector<FileDataLoader::Node>
FileDataLoaderHostFS::GetFolderContents(std::string_view external_relative_path)
{
  auto path = MakeAbsoluteFromRelative(external_relative_path);
  if (!path)
    return {};
  ::File::FSTEntry external_files = ::File::ScanDirectoryTree(*path, false);
  std::vector<FileDataLoader::Node> nodes;
  nodes.reserve(external_files.children.size());
  for (auto& file : external_files.children)
    nodes.emplace_back(FileDataLoader::Node{std::move(file.virtualName), file.isDirectory});
  return nodes;
}

BuilderContentSource
FileDataLoaderHostFS::MakeContentSource(std::string_view external_relative_path,
                                        u64 external_offset, u64 external_size, u64 disc_offset)
{
  auto path = MakeAbsoluteFromRelative(external_relative_path);
  if (!path)
    return BuilderContentSource{disc_offset, external_size, ContentFixedByte{}};
  return BuilderContentSource{disc_offset, external_size,
                              ContentFile{std::move(*path), external_offset}};
}

std::optional<std::string>
FileDataLoaderHostFS::ResolveSavegameRedirectPath(std::string_view external_relative_path)
{
  return MakeAbsoluteFromRelative(external_relative_path);
}

// 'before' and 'after' should be two copies of the same source
// 'split_at' needs to be between the start and end of the source, may not match either boundary
static void SplitAt(BuilderContentSource* before, BuilderContentSource* after, u64 split_at)
{
  const u64 start = before->m_offset;
  const u64 size = before->m_size;
  const u64 end = start + size;

  // The source before the split point just needs its length reduced.
  before->m_size = split_at - start;

  // The source after the split needs its length reduced and its start point adjusted.
  after->m_offset += before->m_size;
  after->m_size = end - split_at;
  if (std::holds_alternative<ContentFile>(after->m_source))
  {
    std::get<ContentFile>(after->m_source).m_offset += before->m_size;
  }
  else if (std::holds_alternative<ContentMemory>(after->m_source))
  {
    after->m_source = std::make_shared<std::vector<u8>>(
        std::get<ContentMemory>(after->m_source)->begin() + before->m_size,
        std::get<ContentMemory>(after->m_source)->end());
  }
  else if (std::holds_alternative<ContentPartition>(after->m_source))
  {
    std::get<ContentPartition>(after->m_source).m_offset += before->m_size;
  }
  else if (std::holds_alternative<ContentVolume>(after->m_source))
  {
    std::get<ContentVolume>(after->m_source).m_offset += before->m_size;
  }
}

static void ApplyPatchToFile(const Patch& patch, DiscIO::FSTBuilderNode* file_node,
                             std::string_view external_filename, u64 file_patch_offset,
                             u64 raw_external_file_offset, u64 file_patch_length, bool resize)
{
  const auto f = patch.m_file_data_loader->GetExternalFileSize(external_filename);
  if (!f)
    return;

  auto& content = std::get<std::vector<BuilderContentSource>>(file_node->m_content);

  const u64 raw_external_filesize = *f;
  const u64 external_file_offset = std::min(raw_external_file_offset, raw_external_filesize);
  const u64 external_filesize = raw_external_filesize - external_file_offset;

  const u64 patch_start = file_patch_offset;
  const u64 patch_size = file_patch_length == 0 ? external_filesize : file_patch_length;
  const u64 patch_end = patch_start + patch_size;

  const u64 target_filesize = resize ? patch_end : std::max(file_node->m_size, patch_end);

  size_t insert_where = 0;
  if (patch_start >= file_node->m_size)
  {
    // If the patch is at or past the end of the existing file no existing content needs to be
    // touched, just extend the file.
    if (patch_start > file_node->m_size)
    {
      // Insert an padding area between the old file and the patch data.
      content.emplace_back(BuilderContentSource{file_node->m_size, patch_start - file_node->m_size,
                                                ContentFixedByte{}});
    }

    insert_where = content.size();
  }
  else
  {
    // Patch is at the start or somewhere in the middle of the existing file. At least one source
    // needs to be modified or removed, and a new source with the patch data inserted instead.
    // To make this easier, we first split up existing sources at the patch start and patch end
    // offsets, then discard all overlapping sources and insert the patch sources there.
    for (size_t i = 0; i < content.size(); ++i)
    {
      const u64 source_start = content[i].m_offset;
      const u64 source_end = source_start + content[i].m_size;
      if (patch_start > source_start && patch_start < source_end)
      {
        content.insert(content.begin() + i + 1, content[i]);
        SplitAt(&content[i], &content[i + 1], patch_start);
        continue;
      }
      if (patch_end > source_start && patch_end < source_end)
      {
        content.insert(content.begin() + i + 1, content[i]);
        SplitAt(&content[i], &content[i + 1], patch_end);
      }
    }

    // Now discard the overlapping areas and remember where they were so we can insert there.
    for (size_t i = 0; i < content.size(); ++i)
    {
      if (patch_start == content[i].m_offset)
      {
        insert_where = i;
        while (i < content.size() && patch_end >= content[i].m_offset + content[i].m_size)
          ++i;
        content.erase(content.begin() + insert_where, content.begin() + i);
        break;
      }
    }
  }

  // Insert the actual patch data.
  if (patch_size > 0 && external_filesize > 0)
  {
    BuilderContentSource source = patch.m_file_data_loader->MakeContentSource(
        external_filename, external_file_offset, std::min(patch_size, external_filesize),
        patch_start);
    content.emplace(content.begin() + insert_where, std::move(source));
    ++insert_where;
  }

  // Pad with zeroes if the patch file is smaller than the patch size.
  if (external_filesize < patch_size)
  {
    BuilderContentSource padding{patch_start + external_filesize, patch_size - external_filesize,
                                 ContentFixedByte{}};
    content.emplace(content.begin() + insert_where, std::move(padding));
  }

  // Update the filesize of the file.
  file_node->m_size = target_filesize;

  // Drop any source past the new end of the file -- this can happen on file truncation.
  while (!content.empty() && content.back().m_offset >= target_filesize)
    content.pop_back();
}

static void ApplyPatchToFile(const Patch& patch, const File& file_patch,
                             DiscIO::FSTBuilderNode* file_node)
{
  // The last two bits of the offset seem to be ignored by actual Riivolution.
  ApplyPatchToFile(patch, file_node, file_patch.m_external, file_patch.m_offset & ~u64(3),
                   file_patch.m_fileoffset, file_patch.m_length, file_patch.m_resize);
}

static FSTBuilderNode* FindFileNodeInFST(std::string_view path, std::vector<FSTBuilderNode>* fst,
                                         bool create_if_not_exists)
{
  const size_t path_separator = path.find('/');
  const bool is_file = path_separator == std::string_view::npos;
  const std::string_view name = is_file ? path : path.substr(0, path_separator);
  const auto it = std::ranges::find_if(*fst, [&](const FSTBuilderNode& node) {
    return Common::CaseInsensitiveEquals(node.m_filename, name);
  });

  if (it == fst->end())
  {
    if (!create_if_not_exists)
      return nullptr;

    if (is_file)
    {
      return &fst->emplace_back(
          DiscIO::FSTBuilderNode{std::string(name), 0, std::vector<BuilderContentSource>()});
    }

    auto& new_folder = fst->emplace_back(
        DiscIO::FSTBuilderNode{std::string(name), 0, std::vector<FSTBuilderNode>()});
    return FindFileNodeInFST(path.substr(path_separator + 1),
                             &std::get<std::vector<FSTBuilderNode>>(new_folder.m_content), true);
  }

  const bool is_existing_node_file = it->IsFile();
  if (is_file != is_existing_node_file)
    return nullptr;
  if (is_file)
    return &*it;

  return FindFileNodeInFST(path.substr(path_separator + 1),
                           &std::get<std::vector<FSTBuilderNode>>(it->m_content),
                           create_if_not_exists);
}

static DiscIO::FSTBuilderNode* FindFilenameNodeInFST(std::string_view filename,
                                                     std::vector<FSTBuilderNode>& fst)
{
  for (FSTBuilderNode& node : fst)
  {
    if (node.IsFolder())
    {
      DiscIO::FSTBuilderNode* result = FindFilenameNodeInFST(filename, node.GetFolderContent());
      if (result)
        return result;
    }
    else if (Common::CaseInsensitiveEquals(node.m_filename, filename))
    {
      return &node;
    }
  }

  return nullptr;
}

static void ApplyFilePatchToFST(const Patch& patch, const File& file,
                                std::vector<DiscIO::FSTBuilderNode>* fst,
                                DiscIO::FSTBuilderNode* dol_node)
{
  if (!file.m_disc.empty() && file.m_disc[0] == '/')
  {
    // If the disc path starts with a / then we should patch that specific disc path.
    DiscIO::FSTBuilderNode* node =
        FindFileNodeInFST(std::string_view(file.m_disc).substr(1), fst, file.m_create);
    if (node)
      ApplyPatchToFile(patch, file, node);
  }
  else if (dol_node && Common::CaseInsensitiveEquals(file.m_disc, "main.dol"))
  {
    // Special case: If the filename is "main.dol", we want to patch the main executable.
    ApplyPatchToFile(patch, file, dol_node);
  }
  else
  {
    // Otherwise we want to patch the first file in the FST that matches that filename.
    DiscIO::FSTBuilderNode* node = FindFilenameNodeInFST(file.m_disc, *fst);
    if (node)
      ApplyPatchToFile(patch, file, node);
  }
}

static void ApplyFolderPatchToFST(const Patch& patch, const Folder& folder,
                                  std::vector<DiscIO::FSTBuilderNode>* fst,
                                  DiscIO::FSTBuilderNode* dol_node, std::string_view disc_path,
                                  std::string_view external_path)
{
  const auto external_files = patch.m_file_data_loader->GetFolderContents(external_path);
  for (const auto& child : external_files)
  {
    const auto combine_paths = [](std::string_view a, std::string_view b) {
      if (a.empty())
        return std::string(b);
      if (b.empty())
        return std::string(a);
      if (a.ends_with('/'))
        a.remove_suffix(1);
      if (b.starts_with('/'))
        b.remove_prefix(1);
      return fmt::format("{}/{}", a, b);
    };
    std::string child_disc_path = combine_paths(disc_path, child.m_filename);
    std::string child_external_path = combine_paths(external_path, child.m_filename);

    if (child.m_is_directory)
    {
      if (folder.m_recursive)
        ApplyFolderPatchToFST(patch, folder, fst, dol_node, child_disc_path, child_external_path);
    }
    else
    {
      File file;
      file.m_disc = std::move(child_disc_path);
      file.m_external = std::move(child_external_path);
      file.m_resize = folder.m_resize;
      file.m_create = folder.m_create;
      file.m_length = folder.m_length;
      ApplyFilePatchToFST(patch, file, fst, dol_node);
    }
  }
}

static void ApplyFolderPatchToFST(const Patch& patch, const Folder& folder,
                                  std::vector<DiscIO::FSTBuilderNode>* fst,
                                  DiscIO::FSTBuilderNode* dol_node)
{
  ApplyFolderPatchToFST(patch, folder, fst, dol_node, folder.m_disc, folder.m_external);
}

void ApplyPatchesToFiles(std::span<const Patch> patches, PatchIndex index,
                         std::vector<FSTBuilderNode>* fst, FSTBuilderNode* dol_node)
{
  for (const auto& patch : patches)
  {
    const auto& file_patches =
        index == PatchIndex::DolphinSysFiles ? patch.m_sys_file_patches : patch.m_file_patches;
    const auto& folder_patches =
        index == PatchIndex::DolphinSysFiles ? patch.m_sys_folder_patches : patch.m_folder_patches;

    for (const auto& file : file_patches)
      ApplyFilePatchToFST(patch, file, fst, dol_node);

    for (const auto& folder : folder_patches)
      ApplyFolderPatchToFST(patch, folder, fst, dol_node);
  }
}

static bool MemoryMatchesAt(const Core::CPUThreadGuard& guard, u32 offset,
                            std::span<const u8> value)
{
  for (u32 i = 0; i < value.size(); ++i)
  {
    auto result = PowerPC::MMU::HostTryReadU8(guard, offset + i);
    if (!result || result->value != value[i])
      return false;
  }
  return true;
}

static void ApplyMemoryPatch(const Core::CPUThreadGuard& guard, u32 offset,
                             std::span<const u8> value, std::span<const u8> original)
{
  if (AchievementManager::GetInstance().IsHardcoreModeActive())
    return;

  if (value.empty())
    return;

  if (!original.empty() && !MemoryMatchesAt(guard, offset, original))
    return;

  auto& system = guard.GetSystem();
  const u32 size = static_cast<u32>(value.size());
  for (u32 i = 0; i < size; ++i)
    PowerPC::MMU::HostTryWriteU8(guard, value[i], offset + i);
  const u32 overlapping_hook_count = HLE::UnpatchRange(system, offset, offset + size);
  if (overlapping_hook_count != 0)
  {
    WARN_LOG_FMT(OSHLE, "Riivolution memory patch overlaps {} HLE hook(s) at {:08x} (size: {})",
                 overlapping_hook_count, offset, value.size());
  }
}

static std::vector<u8> GetMemoryPatchValue(const Patch& patch, const Memory& memory_patch)
{
  if (!memory_patch.m_valuefile.empty())
    return patch.m_file_data_loader->GetFileContents(memory_patch.m_valuefile);
  return memory_patch.m_value;
}

static void ApplyMemoryPatch(const Core::CPUThreadGuard& guard, const Patch& patch,
                             const Memory& memory_patch)
{
  if (memory_patch.m_offset == 0)
    return;

  ApplyMemoryPatch(guard, memory_patch.m_offset | 0x80000000,
                   GetMemoryPatchValue(patch, memory_patch), memory_patch.m_original);
}

static void ApplySearchMemoryPatch(const Core::CPUThreadGuard& guard, const Patch& patch,
                                   const Memory& memory_patch, u32 ram_start, u32 length)
{
  if (memory_patch.m_original.empty() || memory_patch.m_align == 0)
    return;

  const u32 stride = memory_patch.m_align;
  for (u32 i = 0; i < length - (stride - 1); i += stride)
  {
    const u32 address = ram_start + i;
    if (MemoryMatchesAt(guard, address, memory_patch.m_original))
    {
      ApplyMemoryPatch(guard, address, GetMemoryPatchValue(patch, memory_patch), {});
      break;
    }
  }
}

static void ApplyOcarinaMemoryPatch(const Core::CPUThreadGuard& guard, const Patch& patch,
                                    const Memory& memory_patch, u32 ram_start, u32 length)
{
  if (memory_patch.m_offset == 0)
    return;
  const std::vector<u8> value = GetMemoryPatchValue(patch, memory_patch);
  if (value.empty())
    return;

  auto& system = guard.GetSystem();
  for (u32 i = 0; i < length; i += 4)
  {
    // first find the pattern
    const u32 address = ram_start + i;
    if (MemoryMatchesAt(guard, address, value))
    {
      for (; i < length; i += 4)
      {
        // from the pattern find the next blr instruction
        const u32 blr_address = ram_start + i;
        auto blr = PowerPC::MMU::HostTryReadU32(guard, blr_address);
        if (blr && blr->value == 0x4e800020)
        {
          // and replace it with a jump to the given offset
          const u32 target = memory_patch.m_offset | 0x80000000;
          const u32 jmp = ((target - blr_address) & 0x03fffffc) | 0x48000000;
          PowerPC::MMU::HostTryWriteU32(guard, jmp, blr_address);
          const u32 overlapping_hook_count =
              HLE::UnpatchRange(system, blr_address, blr_address + 4);
          if (overlapping_hook_count != 0)
          {
            WARN_LOG_FMT(OSHLE, "Riivolution ocarina patch overlaps HLE hook at {}", blr_address);
          }
          return;
        }
      }
      return;
    }
  }
}

void ApplyGeneralMemoryPatches(const Core::CPUThreadGuard& guard, std::span<const Patch> patches)
{
  const auto& system = guard.GetSystem();
  const auto& system_memory = system.GetMemory();

  for (const auto& patch : patches)
  {
    for (const auto& memory : patch.m_memory_patches)
    {
      if (memory.m_ocarina)
        continue;

      if (memory.m_search)
        ApplySearchMemoryPatch(guard, patch, memory, 0x80000000, system_memory.GetRamSize());
      else
        ApplyMemoryPatch(guard, patch, memory);
    }
  }
}

void ApplyApploaderMemoryPatches(const Core::CPUThreadGuard& guard, std::span<const Patch> patches,
                                 u32 ram_address, u32 ram_length)
{
  for (const auto& patch : patches)
  {
    for (const auto& memory : patch.m_memory_patches)
    {
      if (!memory.m_ocarina && !memory.m_search)
        continue;

      if (memory.m_ocarina)
        ApplyOcarinaMemoryPatch(guard, patch, memory, ram_address, ram_length);
      else
        ApplySearchMemoryPatch(guard, patch, memory, ram_address, ram_length);
    }
  }
}

std::optional<SavegameRedirect> ExtractSavegameRedirect(std::span<const Patch> riivolution_patches)
{
  for (const auto& patch : riivolution_patches)
  {
    if (!patch.m_savegame_patches.empty())
    {
      const auto& save_patch = patch.m_savegame_patches[0];
      auto resolved = patch.m_file_data_loader->ResolveSavegameRedirectPath(save_patch.m_external);
      if (resolved)
        return SavegameRedirect{std::move(*resolved), save_patch.m_clone};
      return std::nullopt;
    }
  }
  return std::nullopt;
}
}  // namespace DiscIO::Riivolution