[Environment]::CurrentDirectory = (Get-Location -PSProvider FileSystem).ProviderPath #Region Variables $input_rom_file = "bin/m12fresh.gba" $output_rom_file = "bin/m12.gba" $test_rom_file = "bin/m12test.gba" $eb_rom_file = "bin/eb.smc" $working_dir = "working" $give_dir = "working/m12-give-strings" $src_dir = "src" $data_dir = "src/data" $give_new_dir = "src/m12-give-strings" $cast_roll_file = "working/cast_roll.json" $default_names_file = "working/default_names.json" $staff_credits_file = "working/staff_text.md" $compiled_asm_file = "src/m2-compiled.asm" $includes_asm_file = "m12-includes.asm" # implicitly rooted in working_dir $hack_asm_file = "m2-hack.asm" # implicitly rooted in src_dir $file_hack_asm_file = "src/m2-file.asm" $file_inc_asm_file = "working/m2-file.asm" $input_c_files = "src/c/ext.c", "src/c/vwf.c", "src/c/locs.c", "src/c/credits.c", "src/c/goods.c", "src/c/fileselect.c", "src/c/status.c", "src/c/battle.c", "src/c/equip.c", "src/c/psi.c", "src/c/title.c", "src/c/luminehall.c", "src/c/custom_codes.c", "src/c/first_func.c", "src/c/window.c" $input_c_test_files = "src/c/tests/main_test.c", "src/c/tests/test_utils.c", "src/c/tests/test_m2_utils.c", "src/c/tests/battle_test.c", "src/c/tests/menu_test.c", "src/c/tests/debug_printf/test_print.c", "src/c/tests/debug_printf/mgba.c", "src/c/tests/debug_printf/printf.c" $base_c_address = 0x83755B8; $scripttool_cmd = "bin/ScriptTool/ScriptTool.dll" $rendercastroll_cmd = "bin/RenderCastRoll/RenderCastRoll.dll" $setdefaultnames_cmd = "bin/SetDefaultNames/SetDefaultNames.dll" $renderstaffcredits_cmd = "bin/RenderStaffCredits/RenderStaffCredits.dll" $gcc_cmd = "arm-none-eabi-gcc" $ld_cmd = "arm-none-eabi-ld" $objdump_cmd = "arm-none-eabi-objdump" $readelf_cmd = "arm-none-eabi-readelf" $combined_obj_file = "src/c/combined.o" $linked_obj_file = "src/c/linked.o" $combine_script = "src/c/combine.ld" $link_script = "src/c/link.ld" $undefine_obj_file = "src/c/ext.o" If ($IsWindows) { $asm_cmd = "bin/armips.exe" } ElseIf ($IsLinux -or $IsMacOS) { $asm_cmd = "bin/armips" } $includes_sym_file = [IO.Path]::ChangeExtension($includes_asm_file, "sym") $output_rom_sym_file = [IO.Path]::ChangeExtension($output_rom_file, "sym") $test_rom_sym_file = [IO.Path]::ChangeExtension($test_rom_file, "sym") $hack_sym_file = [IO.Path]::ChangeExtension($hack_asm_file, "sym") $build_possible_args = "-t", "" $scripttool_args = "-compile", "-main", "-misc", $working_dir, $eb_rom_file, $input_rom_file $rendercastroll_args = $cast_roll_file, $data_dir $setdefaultnames_args = $default_names_file, $data_dir $renderstaffcredits_args = $staff_credits_file, $data_dir $gcc_args = "-c", "-O1", "-fno-ipa-cp", "-fno-inline", "-march=armv4t", "-mtune=arm7tdmi", "-mthumb", "-ffixed-r12", "-mno-long-calls" $gcc_test_args= "-DTEST" $combine_script_contents = "SECTIONS { .text 0x$($base_c_address.ToString('X')) : { *(.text .rodata) } }" $link_script_contents = "SECTIONS { .text 0x$($base_c_address.ToString('X')) : { *(.text .data .rodata*) } }" #EndRegion Variables #Region Functions class Symbol { [string]$Name [int]$Value [int]$Size [bool]$IsLocal [bool]$IsGlobal [bool]$IsWeak [bool]$IsConstructor [bool]$IsWarning [bool]$IsIndirect [bool]$IsDebugging [bool]$IsDynamic [bool]$IsFunction [bool]$IsFile [bool]$IsObject [string]$Section [bool]$IsAbsolute [bool]$IsUndefined } class SectionInfo { [string]$Name [int]$Address [int]$Offset [int]$Size } Function Get-Symbols ([string]$obj_file) { return & $objdump_cmd -t $obj_file | ForEach-Object { New-Symbol $_ } | Where-Object { $_ -ne $null } } # Converts a symbol from objdump's string representation to a rich object representation $symbol_regex = "(?'value'[0-9a-fA-F]{8})\s(?'flags'.{7})\s(?'section'\S+)\s+(?'size'[0-9a-fA-F]{8})\s(?'name'\S+)" Function New-Symbol([string]$symbol_string) { if ($symbol_string -match $symbol_regex) { $symbol = [Symbol]::new() $symbol.Name = $Matches.name $symbol.Value = [int]::Parse($Matches.value, [System.Globalization.NumberStyles]::HexNumber) $symbol.Size = [int]::Parse($Matches.size, [System.Globalization.NumberStyles]::HexNumber) $symbol.Section = $Matches.section $symbol.IsAbsolute = $symbol.Section -eq "*ABS*" $symbol.IsUndefined = $symbol.Section -eq "*UND*" $flags = $Matches.flags $symbol.IsLocal = $flags.Contains("l") -or $flags.Contains("!") $symbol.IsGlobal = $flags.Contains("g") -or $flags.Contains("!") $symbol.IsWeak = $flags.Contains("w") $symbol.IsConstructor = $flags.Contains("C") $symbol.IsWarning = $flags.Contains("W") $symbol.IsIndirect = $flags.Contains("I") $symbol.IsDebugging = $flags.Contains("d") $symbol.IsDynamic = $flags.Contains("D") $symbol.IsFunction = $flags.Contains("F") $symbol.IsFile = $flags.Contains("f") $symbol.IsObject = $flags.Contains("O") return $symbol } else { return $null } } Function Get-SymfileSymbols([string]$symbol_file) { return Get-Content $symbol_file | ForEach-Object { New-SymfileSymbol $_ } | Where-Object { $null -ne $_ } } $symfile_symbol_regex = "(?'value'[0-9a-fA-F]{8})\s+(?'name'(?>\.|@@|[a-zA-Z0-9_])[a-zA-Z0-9_]+):{0,1}(?'size'[0-9a-fA-F]+){0,1}" Function New-SymfileSymbol([string]$symbol_string) { if ($symbol_string -match $symfile_symbol_regex) { $symbol = [Symbol]::new() $symbol.Name = $Matches.name $symbol.Value = [int]::Parse($Matches.value, [System.Globalization.NumberStyles]::HexNumber) if ($null -ne $Matches.size) { $symbol.Size = [int]::Parse($Matches.size, [System.Globalization.NumberStyles]::HexNumber) } else { $symbol.Size = 0 } $symbol.IsLocal = $symbol.Name.StartsWith("@@") $symbol.IsGlobal = -not $symbol.Name.StartsWith(".") -and -not $symbol.IsLocal return $symbol } else { return $null } } function Get-SectionInfo([string]$object_file) { $hash = @{} & $readelf_cmd -S $object_file | ForEach-Object { $section = New-Section $_ if ($null -ne $section) { $hash[$section.Name] = $section } } return $hash } $section_regex = "\s?\[\s?\d+]\s(?'name'\S+)\s+\S+\s+(?'address'[0-9a-fA-F]+)\s(?'offset'[0-9a-fA-F]+)\s(?'size'[0-9a-fA-F]+)" function New-Section([string]$section_string) { if ($section_string -match $section_regex) { $section = [SectionInfo]::new() $section.Name = $Matches.name $section.Address = [int]::Parse($Matches.address, [System.Globalization.NumberStyles]::HexNumber) $section.Offset = [int]::Parse($Matches.offset, [System.Globalization.NumberStyles]::HexNumber) $section.Size = [int]::Parse($Matches.size, [System.Globalization.NumberStyles]::HexNumber) return $section } else { return $null } } #EndRegion Functions <# This is a complicated build script that does complicated things, but it's that way for a reason. - We want to use ASM and C code files simultaneously - The ASM code defines symbols that we want to reference from C - The C code defines symbols that we want to reference from ASM - The game text defines symbols that we want to reference from C The ASM and C code therefore depend on each other. The way around this catch-22 is to separate the compiling and linking stages of the C code. 1) Compile the game text Inputs: - Text/script files Outputs: - BIN files containing compiled text data - ASM files that relocate text pointers (m12-includes.asm) 2) Assemble output from step 1 Inputs: - Output from step 1 - Fresh M12 ROM file Outputs: - ROM file with text inserted and repointed - m12-includes.sym file containing generated symbols (e.g. individual strings from m12-other.json) 3) Compile C code Inputs: - C files Outputs: - O files (one for each C file) Remarks: - All symbols not defined in the C code itself must be marked extern. We will link it in a later step. This includes the string symbols from step 2. - Due to an assembler limitation, extern symbols cannot contain capital letters. - There's a weird quirk with the linker that requires extra care when using external functions. You need to declare them as extern AND implement them using the ((naked)) attribute, e.g. (in a header file) extern int m2_drawwindow(WINDOW* window); (in a code file) int __attribute__((naked)) m2_drawwindow(WINDOW* window) {} See: http://stackoverflow.com/a/43283331/1188632 This will cause a duplicate definition of the function symbol, since it's *really* defined in the ASM files somewhere, but we're redefining it again in C. So we also need to "undefine" these symbols later on when linking. To make it a bit easier to do all that, place all such implementations in ext.c. 4) First link stage Inputs: - O files from step 3 - Base address Outputs: - Single O file positioned to the base address - m2-compiled.asm, containing C symbol definitions Remarks: - This is an incremental link; there will still be undefined symbols. - However, with this combined O file, the code layout will not change and we can now define symbols from the C code to be used in the ASM code. - The symbols will be passed to the assembler next, so export them as an ASM file with one ".definelabel" entry for each defined symbol. They need to be halfword-aligned. This file is called "m2-compiled.asm" by default and is referenced by m2-hack.asm. - Exclude the symbols defined in ext.c from m2-compiled.asm. 5) Assemble ASM code Inputs: - m2-hack.asm - m2-compiled.asm - All other ASM and data files from src/ (but not the generated ones from working/) - M12 ROM file from step 2 Outputs: - M12 ROM file with all ASM code and data included - m2-hack.sym with all symbols defined thusfar 6) Generate final linker script Inputs: - Base address (same as step 4) - m2-hack.sym (from step 5) - m12-includes.sym (from step 2) Outputs: - Linker script file Remarks: - The linker script must define each symbol that's still undefined; if everything is happy at this point, then they should all be contained within the two input SYM files. 7) Final link stage Inputs: - O file from step 4 - Linker script from step 6 Outputs: - Single O file with all symbols defined 8) Copy code to ROM Inputs: - O file from step 7 - M12 ROM file from step 5 Outputs: - M12 ROM file with all code and data included 9) Build final symbol file Inputs: - m2-hack.sym - m12-includes.sym Outputs: - m12.sym Remarks: - This is just the input files concatenated (and sorted for convenience). #> $timer = [System.Diagnostics.StopWatch]::StartNew() #------------------------- HANDLE TEST COMPILING -------------------- if ($args.count -ne 0) { if($args[0] -eq $build_possible_args[0]) { $input_c_files += $input_c_test_files $gcc_args += $gcc_test_args $output_rom_file = $test_rom_file $output_rom_sym_file = $test_rom_sym_file } } # ------------------------- COMPILE GAME TEXT ----------------------- "Copying $input_rom_file to $output_rom_file..." Copy-Item -Path $input_rom_file -Destination $output_rom_file "Compiling game text..." & dotnet $scripttool_cmd $scripttool_args if ($LASTEXITCODE -ne 0) { exit -1 } "Copying give strings to src folder..." Copy-Item -Path $give_dir -Destination $give_new_dir -Recurse "Preparing default names..." & dotnet $setdefaultnames_cmd $setdefaultnames_args if ($LASTEXITCODE -ne 0) { exit -1 } "Pre-rendering cast roll..." & dotnet $rendercastroll_cmd $rendercastroll_args if ($LASTEXITCODE -ne 0) { exit -1 } "Pre-rendering staff credits..." & dotnet $renderstaffcredits_cmd $renderstaffcredits_args if ($LASTEXITCODE -ne 0) { exit -1 } # ------------------------ ASSEMBLE GAME TEXT ----------------------- "Assembling game text..." "m12_rom equ `"../$output_rom_file`"" | Out-File -FilePath $file_inc_asm_file & $asm_cmd -root $working_dir -sym $includes_sym_file $includes_asm_file if ($LASTEXITCODE -ne 0) { exit -1 } Remove-Item -Path $file_inc_asm_file # ----------------------------- COMPILE C --------------------------- $obj_files = @() # Invoke gcc on each file individually so that we can specify the output file foreach ($input_c_file in $input_c_files) { $obj_file = [IO.Path]::ChangeExtension($input_c_file, "o") $obj_files += $obj_file "Compiling $input_c_file..." & $gcc_cmd $gcc_args -o $obj_file $input_c_file if ($LASTEXITCODE -ne 0) { exit -1 } } # ----------------------------- 1ST LINK ---------------------------- "Writing $combine_script..." $combine_script_contents | Out-File -FilePath $combine_script "Linking $obj_files..." & $ld_cmd -i -T $combine_script -o $combined_obj_file $obj_files if ($LASTEXITCODE -ne 0) { exit -1 } "Reading symbols from $combined_obj_file..." $combined_symbols = Get-Symbols $combined_obj_file if ($LASTEXITCODE -ne 0) { exit -1 } "Reading symbols from $undefine_obj_file..." $ext_symbols = Get-Symbols $undefine_obj_file if ($LASTEXITCODE -ne 0) { exit -1 } "Exporting C symbols to $compiled_asm_file..." $ext_symbols_names = $ext_symbols | Where-Object { $_.IsFunction -and $_.IsGlobal -and (-not $_.IsUndefined) } | ForEach-Object { $_.Name } $exported_symbols = $combined_symbols | Where-Object { $_.IsFunction -and $_.IsGlobal -and (-not $_.IsUndefined) -and ($ext_symbols_names -notcontains $_.Name) } $exported_symbols | Sort-Object -Property Name | ForEach-Object { ".definelabel $($_.Name),0x$($_.Value.ToString("X"))" } | Set-Content -Path $compiled_asm_file # ------------------------ ASSEMBLE HACK CODE ----------------------- "Assembling $hack_asm_file..." "m12_rom equ `"../$output_rom_file`"" | Out-File -FilePath $file_hack_asm_file & $asm_cmd -root $src_dir -sym $hack_sym_file $hack_asm_file if ($LASTEXITCODE -ne 0) { exit -1 } Remove-Item -Path $file_hack_asm_file # ------------------- GENERATE FINAL LINKER SCRIPT ------------------ "Writing $link_script..." $hack_symbols = Get-SymfileSymbols "$([IO.Path]::Combine($src_dir, $hack_sym_file))" $includes_symbols = Get-SymfileSymbols "$([IO.Path]::Combine($working_dir, $includes_sym_file))" $asm_symbols = ($hack_symbols + $includes_symbols) | Where-Object { $_.IsGlobal } $asm_symbols_names = $asm_symbols | ForEach-Object { $_.Name } foreach ($ext_symbols_name in $ext_symbols_names) { if ($asm_symbols_names -notcontains $ext_symbols_name) { Write-Host "Error: Undefined external symbol $ext_symbols_name" exit -1 } } Set-Content -Path $link_script -Value $link_script_contents $asm_symbols | ForEach-Object { Add-Content -Path $link_script -Value "$($_.Name) = 0x$($_.Value.ToString("X"));" } # ---------------------------- FINAL LINK --------------------------- "Linking to $linked_obj_file..." & $ld_cmd -T $link_script -o $linked_obj_file $combined_obj_file if ($LASTEXITCODE -ne 0) { exit -1 } # -------------------- COPY COMPILED C CODE TO ROM ------------------ "Copying compiled code to $output_rom_file..." $sections = Get-SectionInfo $linked_obj_file if ($LASTEXITCODE -ne 0) { exit -1 } $text_section = $sections[".text"] $linked_bytes = [IO.File]::ReadAllBytes($linked_obj_file) $rom_bytes = [IO.File]::ReadAllBytes($output_rom_file) [System.Array]::Copy($linked_bytes, $text_section.Offset, $rom_bytes, $text_section.Address - 0x8000000, $text_section.Size) [IO.File]::WriteAllBytes($output_rom_file, $rom_bytes) # -------------------------- GENERATE SYMBOLS ----------------------- "Generating $output_rom_sym_file..." ($hack_symbols + $includes_symbols) | Sort-Object Name | ForEach-Object { "$($_.Value.ToString("X8")) $($_.Name)" } | Set-Content $output_rom_sym_file "Finished compiling $output_rom_file in $($timer.Elapsed.TotalSeconds.ToString("F3")) s" Remove-Item -Path $give_new_dir -Recurse exit 0