diff --git a/.gitignore b/.gitignore index c1b9ffcd..c4e4f46f 100644 --- a/.gitignore +++ b/.gitignore @@ -40,6 +40,9 @@ CMakeLists.txt.user .idea/vcs.xml .idea/workspace.xml +#Visual Studio +.vs + #Dew .dew/* diff --git a/resources/quickplay/MP1/v1.088.bin b/resources/quickplay/MP1/v1.088.bin index 87bf7551..23704ab9 100644 Binary files a/resources/quickplay/MP1/v1.088.bin and b/resources/quickplay/MP1/v1.088.bin differ diff --git a/resources/quickplay/MP1/v1.088.map b/resources/quickplay/MP1/v1.088.map new file mode 100644 index 00000000..e41d4871 --- /dev/null +++ b/resources/quickplay/MP1/v1.088.map @@ -0,0 +1,2 @@ +0x8000207c rel_loader_hook +0x8036F8BC PPCSetFpIEEEMode diff --git a/resources/quickplay/MP1/v1.088.rel b/resources/quickplay/MP1/v1.088.rel index 9477479e..a492b401 100644 Binary files a/resources/quickplay/MP1/v1.088.rel and b/resources/quickplay/MP1/v1.088.rel differ diff --git a/resources/quickplay/MP1/v1.110.bin b/resources/quickplay/MP1/v1.110.bin new file mode 100644 index 00000000..0c7dc195 Binary files /dev/null and b/resources/quickplay/MP1/v1.110.bin differ diff --git a/resources/quickplay/MP1/v1.110.map b/resources/quickplay/MP1/v1.110.map new file mode 100644 index 00000000..f4484b84 --- /dev/null +++ b/resources/quickplay/MP1/v1.110.map @@ -0,0 +1,2 @@ +0x8000207c rel_loader_hook +0x803599e4 PPCSetFpIEEEMode \ No newline at end of file diff --git a/resources/quickplay/MP1/v1.111.bin b/resources/quickplay/MP1/v1.111.bin new file mode 100644 index 00000000..a3dc9831 Binary files /dev/null and b/resources/quickplay/MP1/v1.111.bin differ diff --git a/resources/quickplay/MP1/v1.111.map b/resources/quickplay/MP1/v1.111.map new file mode 100644 index 00000000..5c28ef21 --- /dev/null +++ b/resources/quickplay/MP1/v1.111.map @@ -0,0 +1,2 @@ +0x8000207c rel_loader_hook +0x803704ec PPCSetFpIEEEMode \ No newline at end of file diff --git a/resources/quickplay/MP2/v1.028.bin b/resources/quickplay/MP2/v1.028.bin new file mode 100644 index 00000000..43708e63 Binary files /dev/null and b/resources/quickplay/MP2/v1.028.bin differ diff --git a/resources/quickplay/MP2/v1.028.map b/resources/quickplay/MP2/v1.028.map new file mode 100644 index 00000000..a9b7604e --- /dev/null +++ b/resources/quickplay/MP2/v1.028.map @@ -0,0 +1,2 @@ +0x8000207c rel_loader_hook +0x8035712c PPCSetFpIEEEMode diff --git a/resources/quickplay/MP2/v1.028.rel b/resources/quickplay/MP2/v1.028.rel new file mode 100644 index 00000000..9145e848 Binary files /dev/null and b/resources/quickplay/MP2/v1.028.rel differ diff --git a/src/Editor/NDolphinIntegration.cpp b/src/Editor/NDolphinIntegration.cpp index 8a0eb0d5..45693a4e 100644 --- a/src/Editor/NDolphinIntegration.cpp +++ b/src/Editor/NDolphinIntegration.cpp @@ -1,6 +1,7 @@ #include "NDolphinIntegration.h" #include "Editor/MacOSExtras.h" #include "Editor/UICommon.h" +#include "Editor/SDolHeader.h" #include #include @@ -18,7 +19,7 @@ namespace NDolphinIntegration const char* const gkDolphinPathSetting = "Quickplay/DolphinPath"; const char* const gkFeaturesSetting = "Quickplay/Features"; -const char* const gkRelFileName = "EditorQuickplay.rel"; +const char* const gkRelFileName = "patches.rel"; const char* const gkParameterFile = "dbgconfig"; const char* const gkDolPath = "sys/main.dol"; const char* const gkDolBackupPath = "sys/main.original.dol"; @@ -52,6 +53,34 @@ void CQuickplayRelay::QuickplayFinished(int ReturnCode) CQuickplayRelay gQuickplayRelay; +uint32 AssembleBranchInstruction(uint32 instructionAddress, uint32 branchTarget) +{ + int32 jumpOffset = ((int32)branchTarget - (int32)instructionAddress) / 4; + if (jumpOffset < 0) + { + jumpOffset += 1 << 24; + } + return (18 << 26) + (jumpOffset << 2); +} + +std::map LoadSymbols(const TString& mapContents) { + std::map result; + + for (auto& line : mapContents.Split("\n")) + { + auto separator = line.IndexOf(' '); + if (separator > 0) + { + auto address = line.SubString(0, separator); + auto name = line.SubString(separator + 1, line.Length() - separator - 1).Trimmed(); + + result.emplace(name, static_cast(address.ToInt32(16))); + } + } + + return result; +} + /** Attempt to launch quickplay based on the current editor state. */ EQuickplayLaunchResult LaunchQuickplay(QWidget* pParentWidget, CGameProject* pProject, @@ -59,13 +88,19 @@ EQuickplayLaunchResult LaunchQuickplay(QWidget* pParentWidget, { debugf("Launching quickplay..."); - // Check if quickplay is supported for this project + // Check if we have the files needed for this project's target game and version. + // The required files are split into two parts: + // * Quickplay Module: A .rel compiled from https://github.com/AxioDL/PWEQuickplayPatch/ + // * Dol patch for loading RELs: + // - A .bin compiled from https://github.com/aprilwade/randomprime/tree/master/compile_to_ppc/rel_loader + // - A .map, with the symbol for the .bin and the function in the dol to patch. TString QuickplayDir = gDataDir + "resources/quickplay" / ::GetGameShortName(pProject->Game()); TString BuildString = "v" + TString::FromFloat(pProject->BuildVersion()); TString RelFile = QuickplayDir / BuildString + ".rel"; TString PatchFile = QuickplayDir / BuildString + ".bin"; + TString PatchMapFile = QuickplayDir / BuildString + ".map"; - if (!FileUtil::Exists(RelFile) || !FileUtil::Exists(PatchFile)) + if (!FileUtil::Exists(RelFile) || !FileUtil::Exists(PatchFile) || !FileUtil::Exists(PatchMapFile)) { warnf("Quickplay launch failed! Quickplay is not supported for this project."); return EQuickplayLaunchResult::UnsupportedForProject; @@ -122,14 +157,16 @@ EQuickplayLaunchResult LaunchQuickplay(QWidget* pParentWidget, TString DiscSys = pProject->DiscDir(false) / "sys"; std::vector DolData; std::vector PatchData; + TString MapData; bool bLoadedDol = FileUtil::LoadFileToBuffer(DiscSys / "main.dol", DolData); bool bLoadedPatch = FileUtil::LoadFileToBuffer(PatchFile, PatchData); + bool bLoadedMap = FileUtil::LoadFileToString(PatchMapFile, MapData); - if (!bLoadedDol || !bLoadedPatch) + if (!bLoadedDol || !bLoadedPatch || !bLoadedMap) { - warnf("Quickplay launch failed! Failed to load %s into memory.", - bLoadedDol ? "patch data" : "game DOL"); + const char* failedPart = !bLoadedDol ? "game DOL" : (!bLoadedPatch ? "patch data" : "patch symbols"); + warnf("Quickplay launch failed! Failed to load %s into memory.", failedPart); return EQuickplayLaunchResult::Failure; } @@ -149,26 +186,28 @@ EQuickplayLaunchResult LaunchQuickplay(QWidget* pParentWidget, FileUtil::CopyFile(DolPath, DolBackupPath); } + auto symbols = LoadSymbols(MapData); + SDolHeader header(CMemoryInStream(DolData.data(), DolData.size(), EEndian::BigEndian)); + // Append the patch data to the end of the dol uint32 AlignedDolSize = VAL_ALIGN(DolData.size(), 32); uint32 AlignedPatchSize = VAL_ALIGN(PatchData.size(), 32); - uint32 PatchOffset = AlignedDolSize; - uint32 PatchSize = AlignedPatchSize; - DolData.resize(PatchOffset + PatchSize); - memcpy(&DolData[PatchOffset], &PatchData[0], PatchData.size()); + DolData.resize(AlignedDolSize + AlignedPatchSize); + memcpy(&DolData[AlignedDolSize], &PatchData[0], PatchData.size()); + + if (!header.AddTextSection(0x80002000, AlignedDolSize, AlignedPatchSize)) + { + warnf("Quickplay launch failed! Failed to patch DOL. Is it already patched?"); + return EQuickplayLaunchResult::Failure; + } + + uint32 callToHook = symbols["PPCSetFpIEEEMode"] + 4; + uint32 branchTarget = symbols["rel_loader_hook"]; - // These constants are hardcoded for the moment. - // We write the patch to text section 6, which must be at address 0x80002600. - // We hook over the call to LCEnable, which is at offset 0x1D64. CMemoryOutStream Mem(DolData.data(), DolData.size(), EEndian::BigEndian); - Mem.GoTo(0x18); - Mem.WriteLong(PatchOffset); - Mem.GoTo(0x60); - Mem.WriteLong(0x80002600); - Mem.GoTo(0xA8); - Mem.WriteLong(PatchSize); - Mem.GoTo(0x1D64); - Mem.WriteLong(0x4BFFD80D); // this patches in a call to the quickplay bootstrap during game boot process + header.Write(Mem); + Mem.GoTo(header.OffsetForAddress(callToHook)); + Mem.WriteLong(AssembleBranchInstruction(callToHook, branchTarget)); if (!FileUtil::SaveBufferToFile(DolPath, DolData)) { @@ -211,7 +250,8 @@ bool IsQuickplaySupported(CGameProject* pProject) TString BuildString = "v" + TString::FromFloat(pProject->BuildVersion()); TString RelFile = QuickplayDir / BuildString + ".rel"; TString PatchFile = QuickplayDir / BuildString + ".bin"; - return FileUtil::Exists(RelFile) && FileUtil::Exists(PatchFile); + TString MapFile = QuickplayDir / BuildString + ".map"; + return FileUtil::Exists(RelFile) && FileUtil::Exists(PatchFile) && FileUtil::Exists(MapFile); } /** Kill the current quickplay process, if it exists. */ diff --git a/src/Editor/SDolHeader.h b/src/Editor/SDolHeader.h new file mode 100644 index 00000000..3473cdef --- /dev/null +++ b/src/Editor/SDolHeader.h @@ -0,0 +1,106 @@ +#ifndef CDOLHEADER_H +#define CDOLHEADER_H + +#include +#include + +class SDolHeader +{ +public: + static const size_t kNumTextSections = 7; + static const size_t kNumDataSections = 11; + static const size_t kNumSections = kNumTextSections + kNumDataSections; + + struct Section + { + uint32 Offset; + uint32 BaseAddress; + uint32 Size; + + bool IsEmpty() const { + return Size == 0; + } + }; + + Section Sections[kNumSections]; + uint32 BssAddress; + uint32 BssSize; + uint32 EntryPoint; + + explicit SDolHeader(IInputStream& rInput) + { + for (size_t i = 0; i < kNumSections; ++i) + { + Sections[i].Offset = rInput.ReadLong(); + } + for (size_t i = 0; i < kNumSections; ++i) + { + Sections[i].BaseAddress = rInput.ReadLong(); + } + for (size_t i = 0; i < kNumSections; ++i) + { + Sections[i].Size = rInput.ReadLong(); + } + BssAddress = rInput.ReadLong(); + BssSize = rInput.ReadLong(); + EntryPoint = rInput.ReadLong(); + } + + void Write(IOutputStream& rOutput) const + { + for (size_t i = 0; i < kNumSections; ++i) + { + rOutput.WriteLong(Sections[i].Offset); + } + for (size_t i = 0; i < kNumSections; ++i) + { + rOutput.WriteLong(Sections[i].BaseAddress); + } + for (size_t i = 0; i < kNumSections; ++i) + { + rOutput.WriteLong(Sections[i].Size); + } + rOutput.WriteLong(BssAddress); + rOutput.WriteLong(BssSize); + rOutput.WriteLong(EntryPoint); + } + + bool AddTextSection(uint32 address, uint32 fileOffset, uint32 size) + { + if ((size & 0x1f) != 0) + { + warnf("Unable to add text section: Size not 32-bit aligned."); + return false; + } + + for (size_t i = 0; i < kNumTextSections; ++i) + { + if (Sections[i].IsEmpty()) + { + Sections[i].BaseAddress = address; + Sections[i].Offset = fileOffset; + Sections[i].Size = size; + return true; + } + } + + warnf("Unable to add text section: no empty section found."); + return false; + } + + uint32 OffsetForAddress(uint32 address) + { + for (size_t i = 0; i < kNumSections; ++i) + { + auto& sec = Sections[i]; + if (address > sec.BaseAddress && address < sec.BaseAddress + sec.Size) + { + return sec.Offset + (address - sec.BaseAddress); + } + } + warnf("Unable to add section for address: %x", address); + return 0; + } +}; + +#endif // SDOLHEADER_H