mirror of
https://github.com/CraftyBoss/SuperMarioOdysseyOnline.git
synced 2024-11-05 11:15:04 +00:00
268 lines
9.2 KiB
Python
268 lines
9.2 KiB
Python
import os, sys, re, ctypes, struct
|
|
from keystone import *
|
|
|
|
class Patch:
|
|
def __init__(self, offset, length, content):
|
|
self.offset = offset
|
|
self.length = length
|
|
self.content = content
|
|
|
|
# consts
|
|
PATCH_DIR = "patches"
|
|
PATCH_EXTENSION = ".slpatch"
|
|
|
|
NSO_HEADER_LEN = 0x100
|
|
PATCH_CONFIG_DIR = os.path.join(PATCH_DIR, "configs")
|
|
PATCH_NSO_MAP_DIR = os.path.join(PATCH_DIR, "maps")
|
|
PATCH_CONFIG_EXTENSION = ".config"
|
|
|
|
IPS_OUT_DIR_NAME = "starlight_patch_{}"
|
|
IPS_FORMAT = ".ips"
|
|
IPS_HEADER_MAGIC = bytes("IPS32", 'ASCII')
|
|
IPS_EOF_MAGIC = bytes("EEOF", 'ASCII')
|
|
|
|
# globals
|
|
buildVersion = None
|
|
patchConfig = {
|
|
"build_id" : {},
|
|
"nso_load_addr" : {},
|
|
"map_file" : {}
|
|
}
|
|
patchList = {}
|
|
|
|
def initConfig():
|
|
configPath = os.path.join(PATCH_CONFIG_DIR, buildVersion + PATCH_CONFIG_EXTENSION)
|
|
# read config file
|
|
with open(configPath) as configFile:
|
|
curConfigName = None
|
|
for line in configFile:
|
|
line = line.strip()
|
|
configNameLineMatch = re.match(r'\[(.+)\]', line)
|
|
if configNameLineMatch:
|
|
curConfigName = configNameLineMatch.group(1)
|
|
continue
|
|
|
|
if '=' in line:
|
|
configNSO, configValue = line.split('=', 1)
|
|
if not configNSO.isalnum():
|
|
continue
|
|
if '+' in configValue:
|
|
print("genPatch.py error:", line, "awaits implementation")
|
|
sys.exit(-1)
|
|
patchConfig[curConfigName][configNSO] = configValue
|
|
|
|
# read nso map files
|
|
curVerMapDir = os.path.join(PATCH_NSO_MAP_DIR, buildVersion)
|
|
for file in os.listdir(curVerMapDir):
|
|
if file.endswith('.map'):
|
|
with open(os.path.join(curVerMapDir, file), 'r') as f:
|
|
patchConfig['map_file'][file[:-4]] = f.read()
|
|
|
|
# returns symbol at it's loaded addr relative to main
|
|
def getSymAddrFromMap(target, regexStr, symStr):
|
|
mapFile = None
|
|
def getFoundPosAddr(start):
|
|
regexMatch = re.search(regexStr, mapFile[start:])
|
|
if not regexMatch:
|
|
return -1, -1
|
|
|
|
foundLinePos = mapFile.rfind('\n', 0, regexMatch.span()[0] + start) + 1
|
|
foundLineEndPos = mapFile.find('\n', regexMatch.span()[1] + start)
|
|
foundLine = mapFile[foundLinePos:foundLineEndPos].strip()
|
|
foundAddr = int(foundLine[:foundLine.find(" ")].replace("00000000:00000071", ''), 16)
|
|
|
|
return foundLineEndPos, foundAddr
|
|
|
|
if target in patchConfig['map_file']:
|
|
mapFile = patchConfig['map_file'][target]
|
|
loadAddrAdjustment = int(patchConfig["nso_load_addr"][target], 16)
|
|
else:
|
|
mapFile = SLMapFile
|
|
loadAddrAdjustment = int(patchConfig["nso_load_addr"]["subsdk1"], 16)
|
|
|
|
foundPos, firstFoundAddr = getFoundPosAddr(0)
|
|
if foundPos == -1:
|
|
raise NameError
|
|
|
|
# check uniqueness
|
|
while True:
|
|
foundPos, moreFoundAddr = getFoundPosAddr(foundPos)
|
|
if foundPos == -1:
|
|
break
|
|
|
|
if moreFoundAddr != firstFoundAddr:
|
|
print("genPatch.py error:", symStr, "is ambiguous!")
|
|
sys.exit(-1)
|
|
|
|
# map stores signed address relative to starlight as unsigned?
|
|
return loadAddrAdjustment + ctypes.c_long(firstFoundAddr).value
|
|
|
|
def resolveAddressAndTarget(target, symbolStr):
|
|
resolvedAddr = 0
|
|
|
|
# resolve + offset
|
|
plusSplit = symbolStr.split('+', 1)
|
|
if len(plusSplit) > 1:
|
|
symbolStr = plusSplit[0]
|
|
resolvedAddr += int(plusSplit[1], 16)
|
|
|
|
# resolve different nso target with '!' notation
|
|
targetSplit = symbolStr.split('!', 1)
|
|
if len(targetSplit) > 1:
|
|
target = targetSplit[0]
|
|
symbolStr = targetSplit[1]
|
|
else:
|
|
# if symbol is one of the nso targets
|
|
if symbolStr in patchConfig["nso_load_addr"]:
|
|
target = symbolStr
|
|
resolvedAddr += int(patchConfig["nso_load_addr"][symbolStr], 16)
|
|
return target, resolvedAddr
|
|
|
|
# if symbol is in starlight
|
|
funcStr = symbolStr + r'\('
|
|
try:
|
|
addrInStarlight = getSymAddrFromMap("starlight", funcStr, symbolStr)
|
|
# if no exception, then symbolStr is found is starlight
|
|
resolvedAddr += addrInStarlight
|
|
return target, resolvedAddr
|
|
except NameError:
|
|
pass
|
|
|
|
# if symbolStr is already an address
|
|
try:
|
|
resolvedAddr += int(symbolStr, 16)
|
|
resolvedAddr += int(patchConfig["nso_load_addr"][target], 16)
|
|
return target, resolvedAddr
|
|
except ValueError:
|
|
pass
|
|
|
|
# search symbol in map
|
|
regexStr = r'\w*'.join(re.findall(r'\w+', symbolStr))
|
|
try:
|
|
resolvedAddr += getSymAddrFromMap(target, regexStr, symbolStr)
|
|
except NameError:
|
|
print("genPatch.py error: cannot resolve", symbolStr)
|
|
sys.exit(-1)
|
|
|
|
return target, resolvedAddr
|
|
|
|
def getPatchBin(target, patchAddress, patchValueStr):
|
|
# bytes patch
|
|
try:
|
|
patchBin = bytearray.fromhex(patchValueStr)
|
|
return patchBin
|
|
except ValueError:
|
|
pass
|
|
# string patch
|
|
stringMatch = re.search(r'"(.+)"', patchValueStr)
|
|
if stringMatch:
|
|
return bytearray(bytes(stringMatch.group(1), 'utf-8').decode('unicode_escape') + '\0', 'utf-8')
|
|
|
|
# asm patch
|
|
branchNeedResolveMatch = re.match(r'([Bb][Ll]?\s+)([^\#]+$)', patchValueStr)
|
|
if branchNeedResolveMatch:
|
|
target, toAddr = resolveAddressAndTarget(target, branchNeedResolveMatch.group(2))
|
|
patchValueStr = (branchNeedResolveMatch.group(1) + '#' + hex(toAddr - patchAddress))
|
|
|
|
ks = Ks(KS_ARCH_ARM64, KS_MODE_LITTLE_ENDIAN)
|
|
encodedBytes, insCount = ks.asm(patchValueStr)
|
|
return bytearray(encodedBytes)
|
|
|
|
def addPatchToPatchlist(target, patchAddress, patchContent):
|
|
if target not in patchList:
|
|
patchList[target] = []
|
|
patchList[target].append(Patch(
|
|
patchAddress - int(patchConfig["nso_load_addr"][target], 16) + NSO_HEADER_LEN,
|
|
len(patchContent), patchContent ))
|
|
|
|
def addPatchFromFile(patchFilePath):
|
|
PATCH_VERSION_ALL = "all"
|
|
patchVars = {
|
|
"version" : None,
|
|
"target" : "main"
|
|
}
|
|
|
|
with open(patchFilePath) as patchFile:
|
|
fileLinesIter = iter(patchFile)
|
|
isInMultiPatch = False
|
|
while True:
|
|
# read next line
|
|
if isInMultiPatch:
|
|
# multiPatch check already read new line; simply false the flag
|
|
isInMultiPatch = False
|
|
else:
|
|
try:
|
|
line = next(fileLinesIter)
|
|
except StopIteration:
|
|
break
|
|
line = line.split('/', 1)[0].strip()
|
|
|
|
# if is patch variable line, e.g. [version=100]
|
|
patchVarLineMatch = re.match(r'\[(.+)\]', line)
|
|
if patchVarLineMatch:
|
|
patchVarMatches = re.findall(r'(\w+)=(\w+)', patchVarLineMatch.group(1))
|
|
for match in patchVarMatches:
|
|
patchVars[match[0]] = match[1]
|
|
continue
|
|
|
|
# skip all lines not for our version
|
|
if patchVars["version"] != buildVersion and patchVars["version"] != PATCH_VERSION_ALL:
|
|
continue
|
|
|
|
# parse patches
|
|
addressSplit = line.split(' ', 1)
|
|
isInMultiPatch = addressSplit[0].endswith(':')
|
|
if len(addressSplit) < 2 and not isInMultiPatch:
|
|
continue
|
|
|
|
patchTarget, patchAddress = resolveAddressAndTarget(patchVars["target"],
|
|
addressSplit[0] if not isInMultiPatch else addressSplit[0][:-1])
|
|
patchContent = bytearray()
|
|
|
|
if isInMultiPatch:
|
|
try:
|
|
line = next(fileLinesIter).split('/', 1)[0]
|
|
ident = re.search(r'\s+', line).group()
|
|
while True:
|
|
patchContent += getPatchBin(patchTarget, patchAddress + len(patchContent), line.strip())
|
|
line = next(fileLinesIter).split('/', 1)[0]
|
|
if not line.startswith(ident):
|
|
break
|
|
except StopIteration:
|
|
addPatchToPatchlist(patchTarget, patchAddress, patchContent)
|
|
break
|
|
else:
|
|
patchContent = getPatchBin(patchTarget, patchAddress, addressSplit[1])
|
|
|
|
addPatchToPatchlist(patchTarget, patchAddress, patchContent)
|
|
|
|
if len(sys.argv) < 2:
|
|
print('Usage: ' + sys.argv[0] + ' [version]')
|
|
sys.exit(-1)
|
|
|
|
buildVersion = sys.argv[1]
|
|
initConfig()
|
|
SLMapFilePath = os.path.join("build" + buildVersion, os.path.basename(os.getcwd()) + buildVersion + ".map")
|
|
with open(SLMapFilePath, 'r') as f:
|
|
SLMapFile = f.read()
|
|
|
|
for file in os.listdir(PATCH_DIR):
|
|
if file.endswith(PATCH_EXTENSION):
|
|
addPatchFromFile(os.path.join(PATCH_DIR, file))
|
|
|
|
ipsOutDir = IPS_OUT_DIR_NAME.format(buildVersion)
|
|
try:
|
|
os.mkdir(ipsOutDir)
|
|
except FileExistsError:
|
|
pass
|
|
|
|
for nso in patchList:
|
|
ipsOutPath = os.path.join(ipsOutDir, patchConfig["build_id"][nso] + IPS_FORMAT)
|
|
with open(ipsOutPath, 'wb') as ipsFile:
|
|
ipsFile.write(IPS_HEADER_MAGIC)
|
|
for patch in patchList[nso]:
|
|
ipsFile.write(struct.pack('>I', patch.offset))
|
|
ipsFile.write(struct.pack('>H', patch.length))
|
|
ipsFile.write(patch.content)
|
|
ipsFile.write(IPS_EOF_MAGIC)
|
|
print("genPatch.py:", nso, "->", ipsOutPath, "successful")
|