Dynamic-Pokemon-Expansion/scripts/build.py
Skeli eafaebbd14 Shadow Warrior + Manaphy Egg
-Also update Skuntank & Drapion Hidden Abilities
2020-10-05 20:42:19 -04:00

441 lines
15 KiB
Python

#!/usr/bin/env python3
from glob import glob
from pathlib import Path
import os
import itertools
import hashlib
import subprocess
import sys
from datetime import datetime
from string import StringFileConverter
from tm_tutor import TMDataBuilder, TutorDataBuilder
if sys.platform.startswith('win'):
PathVar = os.environ.get('Path')
Paths = PathVar.split(';')
PATH = ''
for candidatePath in Paths:
if 'devkitARM' in candidatePath:
PATH = candidatePath
break
if PATH == '':
print('DevKit does not exist in your Path variable.\nChecking default location.')
PATH = 'C://devkitPro//devkitARM//bin'
if os.path.isdir(PATH) is False:
print('...\nDevkit not found.')
sys.exit(1)
else:
print('Devkit found.')
PREFIX = '/arm-none-eabi-'
AS = PATH + PREFIX + 'as'
CC = PATH + PREFIX + 'gcc'
LD = PATH + PREFIX + 'ld'
GR = 'deps/grit.exe'
WAV2AGB = 'deps/wav2agb.exe'
MID2AGB = 'deps/mid2agb.exe'
OBJCOPY = PATH + PREFIX + 'objcopy'
else: # Linux, OSX, etc.
PREFIX = 'arm-none-eabi-'
AS = PREFIX + 'as'
CC = PREFIX + 'gcc'
LD = PREFIX + 'ld'
GR = "grit"
WAV2AGB = 'wav2agb'
MID2AGB = 'mid2agb'
OBJCOPY = PREFIX + 'objcopy'
SRC = './src'
GRAPHICS = './graphics'
ASSEMBLY = './assembly'
STRINGS = './strings'
AUDIO = './audio'
BUILD = './build'
IMAGES = './Images'
ASFLAGS = ['-mthumb', '-I', ASSEMBLY]
LDFLAGS = ['BPRE.ld', '-T', 'linker.ld']
CFLAGS = ['-mthumb', '-mno-thumb-interwork', '-mcpu=arm7tdmi', '-mtune=arm7tdmi',
'-mno-long-calls', '-march=armv4t', '-Wall', '-Wextra', '-Os', '-fira-loop-pressure', '-fipa-pta']
class Master:
@staticmethod
def init():
Master.printedCompilingImages = False
Master.printedCompilingAudio = False
Master.printedCompilingMusic = False
@staticmethod
def printCompilingImages():
if not Master.printedCompilingImages:
# Used to tell the script whether or not the string 'Compiling Images' has been printed
Master.printedCompilingImages = True
print('Compiling Images')
@staticmethod
def printCompilingAudio():
if not Master.printedCompilingAudio:
# Used to tell the script whether or not the string 'Compiling Audio' has been printed
Master.printedCompilingAudio = True
print('Compiling Audio')
@staticmethod
def printCompilingMusic():
if not Master.printedCompilingMusic:
# Used to tell the script whether or not the string 'Compiling Music' has been printed
Master.printedCompilingMusic = True
print('Compiling Music')
def RunCommand(cmd: [str]):
"""Runs the command line command."""
try:
subprocess.check_output(cmd)
except subprocess.CalledProcessError as e:
try:
print(e.output.decode(), file=sys.stderr)
except:
print(e)
sys.exit(1)
def CreateOutputFile(fileName: str, newFileName: str) -> [str, bool]:
"""Helper function to produce object file output."""
if not os.path.isfile(fileName):
return [newFileName, False]
fileExists = os.path.isfile(newFileName)
# If the object file was created after the file was last modified
if fileExists and os.path.getmtime(newFileName) > os.path.getmtime(fileName):
return [newFileName, False]
return [newFileName, True]
def MakeGeneralOutputFile(fileName: str) -> [str, bool]:
"""Return hash of filename to use as object filename."""
m = hashlib.md5()
m.update(fileName.encode())
newFileName = os.path.join(BUILD, m.hexdigest() + '.o')
return CreateOutputFile(fileName, newFileName)
def MakeOutputImageFile(assemblyFile: str) -> [str, bool]:
"""Return 'IMG_' + hash of filename to use as object filename."""
m = hashlib.md5()
m.update(assemblyFile.encode())
objectFile = os.path.join(BUILD, 'IMG_' + m.hexdigest() + '.o')
return CreateOutputFile(assemblyFile, objectFile)
def MakeOutputAudioFile(assemblyFile: str) -> [str, bool]:
"""Return "SND_" + hash of filename to use as object filename."""
objectFile = os.path.join(BUILD, 'SND_' + assemblyFile.split("gCry")[1].split(".s")[0] + '.o')
return CreateOutputFile(assemblyFile, objectFile)
def MakeOutputMusicFile(assemblyFile: str) -> [str, bool]:
"""Return "MUS_" + hash of filename to use as object filename."""
if sys.platform.startswith('win'): # Windows
objectFile = os.path.join(BUILD, 'MUS_'
+ assemblyFile.split('\\')[len(assemblyFile.split('\\')) - 1].split(".s")[0] + '.o')
else: # Linux, OSX, etc.
objectFile = os.path.join(BUILD, 'MUS_'
+ assemblyFile.split('/')[len(assemblyFile.split('/')) - 1].split(".s")[0] + '.o')
return CreateOutputFile(assemblyFile, objectFile)
def DoMiddleManAssembly(originalFile: str, assemblyFile: str, flagFile: str, flags: [str],
cmd: [str], func, printingFunc, isMusic: bool) -> str:
"""Process assembly files generated by things like grit, wav2agb, or mid2agb."""
objectFile = func(assemblyFile)[0]
fileExists = os.path.isfile(objectFile)
flagFileExists = os.path.isfile(flagFile)
if fileExists \
and os.path.getmtime(objectFile) > os.path.getmtime(originalFile) \
and (not flagFileExists or os.path.getmtime(objectFile) > os.path.getmtime(flagFile)):
# If the .o file was created after the original and flag file were last modified
return objectFile
else: # The original file or the flag file were modified recently
printingFunc()
RunCommand(cmd)
if isMusic: # Try to update the voicegroup manually
counter = 0
lineToChange = ''
with open(assemblyFile, 'r') as file:
for line in file:
counter += 1
if '_grp,' in line:
lineToChange = line.split('voicegroup')[0]
break
if flags != [] and lineToChange != '' and '-G' in flags:
ChangeFileLine(assemblyFile, counter, lineToChange + flags[flags.index('-G') + 1] + '\n')
regenerateObjectFile = func(assemblyFile)[1]
if regenerateObjectFile is False:
os.remove(assemblyFile)
return objectFile # No point in recompiling file
cmd = [AS] + ASFLAGS + ['-c', assemblyFile, '-o', objectFile]
RunCommand(cmd)
os.remove(assemblyFile)
return objectFile
def ProcessAssembly(assemblyFile: str) -> str:
"""Assemble."""
objectFile, regenerateObjectFile = MakeGeneralOutputFile(assemblyFile)
if regenerateObjectFile is False:
return objectFile # No point in recompiling file
try:
print('Assembling %s' % assemblyFile)
cmd = [AS] + ASFLAGS + ['-c', assemblyFile, '-o', objectFile]
RunCommand(cmd)
except FileNotFoundError:
print('Error! The assembler could not be located.\n'
+ 'Are you sure you set up your path to devkitPro/devkitARM/bin correctly?')
sys.exit(1)
return objectFile
def ProcessC(cFile: str) -> str:
"""Compile C."""
objectFile, regenerateObjectFile = MakeGeneralOutputFile(cFile)
if regenerateObjectFile is False:
return objectFile # No point in recompiling file
try:
print('Compiling %s' % cFile)
cmd = [CC] + CFLAGS + ['-c', cFile, '-o', objectFile]
RunCommand(cmd)
except FileNotFoundError:
print('Error! The C compiler could not be located.\n'
+ 'Are you sure you set up your path to devkitPro/devkitARM/bin correctly?')
sys.exit(1)
return objectFile
def ProcessString(stringFile: str) -> str:
"""Build and assemble strings."""
assemblyFile = stringFile.split('.string')[0] + '.s'
objectFile = MakeGeneralOutputFile(assemblyFile)[0]
fileExists = os.path.isfile(objectFile)
if fileExists and os.path.getmtime(objectFile) > os.path.getmtime(stringFile):
# If the .o file was created after the string file was last modified
return objectFile
print('Building Strings %s' % stringFile)
StringFileConverter(stringFile)
cmd = [AS] + ASFLAGS + ['-c', assemblyFile, '-o', objectFile]
RunCommand(cmd)
os.remove(assemblyFile)
return objectFile
def GetFlagsFromFlagFile(filePath: str) -> [str]:
try:
with open(filePath, "r") as file:
line = file.readline() # Only needs the first line
flags = line.split()
except FileNotFoundError:
print('"{}" could not be found.'.format(filePath))
sys.exit(1)
return flags
def ProcessSpriteSet(fileListing: [str], flags: [str], outputFile: str, title: str):
assembledFile = os.path.join(ASSEMBLY, 'generated', outputFile)
if (not os.path.isfile(assembledFile)
or max(list(map(os.path.getmtime, fileListing))) > os.path.getmtime(assembledFile)): # If a sprite has been modified
print("Processing {}.".format(title))
combinedFile = open(assembledFile, 'w')
combinedFile.write('@THIS IS A GENERATED FILE! DO NOT MODIFY IT!\n')
for sprite in fileListing:
assembled = sprite.split('.png')[0] + '.s'
if (not os.path.isfile(assembled)
or os.path.getmtime(sprite) > os.path.getmtime(assembled)):
RunCommand([GR, sprite] + flags + ['-o', assembled])
with open(assembled, 'r') as tempFile:
combinedFile.write(tempFile.read())
combinedFile.close()
def ProcessSpriteGraphics():
frontFlags = GetFlagsFromFlagFile(GRAPHICS + "/frontspriteflags.grit")
backFlags = GetFlagsFromFlagFile(GRAPHICS + "/backspriteflags.grit")
iconFlags = GetFlagsFromFlagFile(GRAPHICS + "/iconspriteflags.grit")
castformFlags = GetFlagsFromFlagFile(GRAPHICS + "/castform/gritflags.txt")
egghatchFlags = GetFlagsFromFlagFile(GRAPHICS + "/egg_hatch/gritflags.txt")
backsprites = [file for file in glob(GRAPHICS + "/backspr" + "**/*.png", recursive=True)]
frontsprites = [file for file in glob(GRAPHICS + "/frontspr" + "**/*.png", recursive=True)]
iconsprites = [file for file in glob(GRAPHICS + "/pokeicon" + "**/*.png", recursive=True)]
castformsprites = [file for file in glob(GRAPHICS + "/castform" + "**/*.png", recursive=True)]
egghatchsprites = [file for file in glob(GRAPHICS + "/egg_hatch" + "**/*.png", recursive=True)]
ProcessSpriteSet(frontsprites, frontFlags, 'frontsprites.s', "Front Sprites")
ProcessSpriteSet(backsprites, backFlags, 'backsprites.s', "Back Sprites")
ProcessSpriteSet(iconsprites, iconFlags, 'iconsprites.s', "Icon Sprites")
ProcessSpriteSet(castformsprites, castformFlags, 'castformsprites.s', "Castform Sprites")
ProcessSpriteSet(egghatchsprites, egghatchFlags, 'egghatchsprites.s', "Egg Hatch Sprites")
def ProcessAudio(audioFile: str) -> str:
"""Compile audio."""
assemblyFile = audioFile.split('.wav')[0] + '.s'
flags = []
flagFile = audioFile.split('.wav')[0] + '_flags.txt'
try:
with open(flagFile, 'r') as file:
line = file.readline() # Only needs the first line
flags = line.strip().split()
except FileNotFoundError:
pass
cmd = [WAV2AGB, audioFile, assemblyFile] + flags
return DoMiddleManAssembly(audioFile, assemblyFile, flagFile, flags, cmd,
MakeOutputAudioFile, Master.printCompilingAudio, False)
def ProcessMusic(midiFile: str) -> str:
"""Compile audio."""
assemblyFile = midiFile.split('.mid')[0] + '.s'
flags = []
flagFile = midiFile.split('.mid')[0] + '_flags.txt'
try:
with open(flagFile, 'r') as file:
line = file.readline() # Only needs the first line
flags = line.strip().split()
except FileNotFoundError:
pass
cmd = [MID2AGB, midiFile, assemblyFile] + flags
return DoMiddleManAssembly(midiFile, assemblyFile, flagFile, flags, cmd,
MakeOutputMusicFile, Master.printCompilingMusic, True)
def LinkObjects(objects: itertools.chain) -> str:
"""Link objects into one binary."""
linked = 'build/linked.o'
cmd = [LD] + LDFLAGS + ['-o', linked] + list(objects)
RunCommand(cmd)
return linked
def Objcopy(binary: str):
"""Run the objcopy."""
cmd = [OBJCOPY, '-O', 'binary', binary, 'build/output.bin']
RunCommand(cmd)
def RunGlob(globString: str, fn) -> map:
"""Glob recursively and run the processor function on each file in result."""
if globString == '**/*.png' or globString == '**/*.bmp': # Search the GRAPHICS location
directory = GRAPHICS
elif globString == '**/*.s':
directory = ASSEMBLY
elif globString == '**/*.string':
directory = STRINGS
elif globString == '**/*.wav' or globString == '**/*.mid':
directory = AUDIO
else:
directory = SRC
if sys.version_info > (3, 4):
try:
files = glob(os.path.join(directory, globString), recursive=True)
return map(fn, files)
except TypeError:
print('Error compiling. Please make sure Python has been updated to the latest version.')
sys.exit(1)
else:
files = Path(directory).glob(globString)
return map(fn, map(str, files))
def main():
Master.init()
startTime = datetime.now()
globs = {
'**/*.s': ProcessAssembly,
'**/*.c': ProcessC,
'**/*.string': ProcessString,
# '**/*.png': ProcessImage,
# '**/*.bmp': ProcessImage,
'**/*.wav': ProcessAudio,
'**/*.mid': ProcessMusic,
}
if sys.version_info.major >= 3 and sys.version_info.minor >= 8:
print("Warning! Python 3.8 may not be able to build this engine.\nPlease downgrade to Python 3.7.4")
# Create output directory
try:
os.makedirs(BUILD)
except FileExistsError:
pass
try:
try:
os.makedirs(ASSEMBLY + "/generated")
except FileExistsError:
pass
ProcessSpriteGraphics()
TMDataBuilder()
TutorDataBuilder()
# Gather source files and process them
objects = itertools.starmap(RunGlob, globs.items())
# Link and extract raw binary
linked = LinkObjects(itertools.chain.from_iterable(objects))
Objcopy(linked)
except Exception as e:
print("There was an error compiling the engine: {}".format(e))
sys.exit(1)
# Build special_inserts.asm
if os.path.isfile('special_inserts.asm'):
if not os.path.isfile('build/special_inserts.bin') \
or os.path.getmtime('build/special_inserts.bin') < os.path.getmtime('special_inserts.asm'):
print('Assembling special_inserts.asm')
cmd = [AS] + ASFLAGS + ['-c', 'special_inserts.asm', '-o', 'build/special_inserts.o']
RunCommand(cmd)
cmd = [OBJCOPY, '-O', 'binary', 'build/special_inserts.o', 'build/special_inserts.bin']
RunCommand(cmd)
print('Built in ' + str(datetime.now() - startTime) + '.')
if __name__ == '__main__':
main()