Last active
April 19, 2023 17:14
-
-
Save JohnnyonFlame/f917d8ca2bba0323f91ba6f3b508b3b4 to your computer and use it in GitHub Desktop.
Convert older GMS games to newer runners w UTMT
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.Windows; | |
using System.Reflection; | |
EnsureDataLoaded(); | |
RunUMTScript(Path.Combine(ExePath, "Scripts/Technical Scripts/15_to_17_To_16.csx")); | |
MainWindow mainWindow = Application.Current.MainWindow as MainWindow; | |
Data.SetGMS2Version(2, 2, 0, 258); | |
foreach (var asset in Data.Backgrounds) | |
{ | |
asset.GMS2TileWidth = asset.Texture.SourceWidth; | |
asset.GMS2TileHeight = asset.Texture.SourceHeight; | |
asset.GMS2OutputBorderX = 1; | |
asset.GMS2OutputBorderY = 1; | |
asset.GMS2TileColumns = 1; | |
asset.GMS2ItemsPerTileCount= 1; | |
asset.GMS2TileCount = 0; | |
} | |
uint largest_layerid = 0; | |
String name_str = "auto_layer"; | |
String name_str_bg = "auto_layer_bg"; | |
var layer_name = Data.Strings.MakeString(name_str); | |
var layer_name_bg = Data.Strings.MakeString(name_str_bg); | |
// FIX ROOMS | |
foreach (var room in Data.Rooms) | |
{ | |
room.Flags=room.Flags | UndertaleRoom.RoomEntryFlags.IsGMS2; | |
UndertaleRoom.Layer layerBg = new() | |
{ | |
LayerName = layer_name_bg, | |
LayerId = (largest_layerid++) + 1, | |
LayerType = UndertaleRoom.LayerType.Assets, | |
LayerDepth = (int)99999, | |
Data = new UndertaleRoom.Layer.LayerAssetsData() | |
}; | |
layerBg.AssetsData.LegacyTiles ??= new UndertalePointerList<UndertaleRoom.Tile>(); | |
layerBg.AssetsData.Sprites ??= new UndertalePointerList<UndertaleRoom.SpriteInstance>(); | |
UndertaleRoom.Layer layerInstances = new() | |
{ | |
LayerName = layer_name, | |
LayerId = (largest_layerid++) + 1, | |
LayerType = UndertaleRoom.LayerType.Instances, | |
LayerDepth = (int)0, | |
Data = new UndertaleRoom.Layer.LayerInstancesData() | |
}; | |
room.Layers.Add(layerBg); | |
room.Layers.Add(layerInstances); | |
foreach (var inst in room.GameObjects) | |
{ | |
if (!(inst is UndertaleModLib.Models.UndertaleRoom.Tile)) | |
layerInstances.InstancesData.Instances.Add(inst); | |
} | |
foreach (var tile in room.Tiles) | |
{ | |
try { | |
var name = "auto_bgt_" + tile.BackgroundDefinition.Name.ToString()[1..^1]; | |
UndertaleSprite spr = Data.Sprites.ByName(name); | |
if (spr == null) { | |
spr = new UndertaleSprite() | |
{ | |
Name = Data.Strings.MakeString(name), | |
Width = tile.BackgroundDefinition.GMS2TileWidth, | |
Height = tile.BackgroundDefinition.GMS2TileHeight, | |
MarginLeft = 0, | |
MarginRight = 0, | |
MarginTop = 0, | |
MarginBottom = 0, | |
IsSpecialType = false, | |
GMS2PlaybackSpeed = 0 | |
}; | |
var tEntry = new UndertaleSprite.TextureEntry(); | |
tEntry.Texture = tile.BackgroundDefinition.Texture; | |
spr.Textures.Add(tEntry); | |
Data.Sprites.Add(spr); | |
} | |
tile.SpriteDefinition = spr; | |
} | |
catch(Exception e) { | |
} | |
tile.spriteMode = true; | |
layerBg.AssetsData.LegacyTiles.Add(tile); | |
} | |
room.Tiles.Clear(); | |
} | |
void add_missing(String name, String func) | |
{ | |
mainWindow.ImportGMLString(name, func, true, true); | |
UndertaleScript scr = new UndertaleScript(); | |
scr.Name = Data.Strings.MakeString(name); | |
scr.Code = Data.Code.ByName(name); | |
Data.Scripts.Add(scr); | |
} | |
add_missing( | |
"instance_create", | |
"return instance_create_layer(argument0, argument1, layer_get_id(\"auto_layer\"), argument2)" | |
); | |
add_missing( | |
"texture_set_interpolation", | |
"gpu_set_texfilter(argument0)" | |
); | |
add_missing("joystick_exists", //(id) | |
@" | |
var gamepad = argument0 + 1; | |
return gamepad_is_connected(gamepad); | |
"); | |
add_missing("joystick_direction", //(id) | |
@" | |
var deadzone = 0.15; | |
var gamepad = argument0 + 1; | |
// Get the X and Y axis values | |
var x_axis = gamepad_axis_value(gamepad, gp_axislh); | |
var y_axis = gamepad_axis_value(gamepad, gp_axislv); | |
// Calculate the direction | |
var direction = -1; | |
var angle = point_direction(0, 0, x_axis, y_axis); | |
if (x_axis * x_axis + y_axis * y_axis > deadzone * deadzone) | |
{ | |
direction = round(angle / 45) mod 8; | |
} | |
return direction; | |
"); | |
add_missing("joystick_name", //(id) | |
@" | |
return ""Xbox 360 Controller (XInput STANDARD GAMEPAD)""; | |
"); | |
add_missing("joystick_axes", //(id) | |
@" | |
var gamepad = argument0 + 1; | |
return gamepad_axis_count(gamepad); | |
"); | |
add_missing("joystick_buttons", //(id) | |
@" | |
var gamepad = argument0 + 1; | |
return gamepad_button_count(gamepad); | |
"); | |
add_missing("joystick_has_pov", //(id) | |
@" | |
return true; | |
"); | |
add_missing("joystick_check_button", //(id,button) | |
@" | |
var gamepad = argument0 + 1; | |
return gamepad_button_check(gamepad, argument1); | |
"); | |
add_missing("joystick_xpos", | |
@" | |
var gamepad = argument0 + 1; | |
return gamepad_axis_value(gamepad, gp_axislh); | |
"); | |
add_missing("joystick_ypos", | |
@" | |
var gamepad = argument0 + 1; | |
return -gamepad_axis_value(gamepad, gp_axislv); | |
"); | |
add_missing("joystick_zpos", | |
@" | |
var gamepad = argument0 + 1; | |
return gamepad_axis_value(gamepad, gp_axisrh); | |
"); | |
add_missing("joystick_rpos", | |
@" | |
var gamepad = argument0 + 1; | |
return -gamepad_axis_value(gamepad, gp_axisrv); | |
"); | |
add_missing("joystick_upos", | |
@" | |
var gamepad = argument0 + 1; | |
return gamepad_axis_value(gamepad, gp_axisuh); | |
"); | |
add_missing("joystick_vpos", | |
@" | |
var gamepad = argument0 + 1; | |
return -gamepad_axis_value(gamepad, gp_axisuv); | |
"); | |
add_missing("joystick_pov", | |
@" | |
var gamepad = argument0 + 1; | |
if (gamepad_is_connected(gamepad)) { | |
var up = gamepad_button_check(gamepad, gp_pov_up); | |
var right = gamepad_button_check(gamepad, gp_pov_right); | |
var down = gamepad_button_check(gamepad, gp_pov_down); | |
var left = gamepad_button_check(gamepad, gp_pov_left); | |
if (up && right) { | |
return 45; | |
} else if (right && down) { | |
return 135; | |
} else if (down && left) { | |
return 225; | |
} else if (left && up) { | |
return 315; | |
} else if (up) { | |
return 0; | |
} else if (right) { | |
return 90; | |
} else if (down) { | |
return 180; | |
} else if (left) { | |
return 270; | |
} else { | |
return -1; | |
} | |
} else { | |
return -1; | |
} | |
"); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.Linq; | |
// var i; | |
// if (x > 10) | |
// draw_text(0, 0, "Heelloooo wooorlllddd.") | |
// else | |
// draw_text(0, 0, "AAAAAAAYYYYYYYY.") | |
// for (i = 0; i < 10; i++) | |
// draw_text(0, 0, "AAAAAAAAAA") | |
// draw_text(0, 0, "DOOOOOOOOANE") | |
// Declare trackers... | |
List<String> needsArgumentPatchingList = new List<String>(); | |
// Helper to set necessary functions that we'll need for patching if they're not known. | |
void defineFunctionIfMissing(String name) | |
{ | |
if (Data.Functions.ByName(name) == null) { | |
var str = Data.Strings.MakeString(name); | |
var func = new UndertaleFunction() | |
{ | |
Name = str, | |
NameStringID = Data.Strings.IndexOf(str) | |
}; | |
Data.Functions.Add(func); | |
} | |
} | |
int getStringOrCreate(String name) | |
{ | |
var byMake = Data.Strings.MakeString(name); | |
return Data.Strings.IndexOf(byMake); | |
} | |
// Helper to pop many, e.g. to pop an entire range for calls | |
List<T> PopRange<T>(ref Stack<T> stack, int amount) | |
{ | |
var result = new List<T>(amount); | |
while (amount-- > 0 && stack.Count > 0) | |
{ | |
result.Add(stack.Pop()); | |
} | |
return result; | |
} | |
// Helper for inserting asm at random places and then relocating branches when necessary | |
int insertCodeAt(string asm, UndertaleCode code, int where) | |
{ | |
var locals = Data.CodeLocals.For(code); | |
var dict = new Dictionary<string, UndertaleVariable>(); | |
foreach (var local in locals.Locals) | |
dict.Add(local.Name.Content, Data.Variables.ByName(local.Name.Content)); | |
List<UndertaleInstruction> newInstructions = new List<UndertaleInstruction>(); | |
foreach (var line in asm.Split("\n", StringSplitOptions.None)) { | |
var sline = line.Trim(); | |
if (sline.Length > 0) | |
newInstructions.Add(Assembler.AssembleOne(sline, Data.Functions, Data.Variables, Data.Strings, dict, Data)); | |
} | |
uint patchSize = 0; | |
foreach (var inst in newInstructions) | |
patchSize += inst.CalculateInstructionSize(); | |
// Address of the instruction where we're inserting new code | |
uint prevAddress = code.Instructions[where].Address; | |
for (int i = 0; i < code.Instructions.Count; i++) { | |
UndertaleInstruction inst = code.Instructions[i]; | |
if (UndertaleInstruction.GetInstructionType(inst.Kind) == UndertaleInstruction.InstructionType.GotoInstruction) { | |
var branchAddress = | |
code.Instructions[i].Address + | |
code.Instructions[i].JumpOffset; | |
if (code.Instructions[i].Address < prevAddress) { | |
if (branchAddress > prevAddress) | |
code.Instructions[i].JumpOffset = code.Instructions[i].JumpOffset + (int)patchSize; | |
} else { | |
if (branchAddress < prevAddress) | |
code.Instructions[i].JumpOffset = code.Instructions[i].JumpOffset - (int)patchSize; | |
} | |
} | |
} | |
code.Instructions.InsertRange(where, newInstructions); | |
code.UpdateAddresses(); | |
return newInstructions.Count; | |
} | |
class FixUpType | |
{ | |
public int where; | |
public String patch; | |
}; | |
// Helper for registering whatever fixups might be necessary | |
void PopAndRegisterFixups(UndertaleInstruction inst, ref Stack<UndertaleInstruction> stack, ref List<FixUpType> FixUp, UndertaleCode code) | |
{ | |
var StackLocs = PopRange(ref stack, inst.ArgumentsCount); | |
switch(inst.Function.ToString()) { | |
case "draw_text": | |
case "draw_text_ext": | |
case "draw_text_transformed": | |
case "draw_text_ext_transformed": | |
case "draw_text_color": | |
case "draw_text_transformed_color": | |
case "draw_text_ext_color": | |
case "draw_text_ext_transformed_color": | |
case "draw_text_colour": | |
case "draw_text_transformed_colour": | |
case "draw_text_ext_colour": | |
case "draw_text_ext_transformed_colour": | |
// Add argument fixup to convert '#' to '\n' | |
FixUp.Add(new FixUpType(){ | |
where = code.Instructions.IndexOf(StackLocs[2]), | |
patch = | |
$"call.i string_hash_to_newline(argc=1)" | |
}); | |
break; | |
default: | |
// Check if this function requires argument lists and fix it up | |
if (needsArgumentPatchingList.Contains(inst.Function.ToString())) { | |
FixUp.Add(new FixUpType() { | |
where = code.Instructions.IndexOf(inst) - 1, | |
patch = | |
$"call.i @@NewGMLArray@@(argc={inst.ArgumentsCount})\n" + | |
$"pushi.e {inst.ArgumentsCount}\n" + | |
$"conv.i.v" | |
}); | |
inst.ArgumentsCount = 2; | |
} | |
break; | |
} | |
} | |
// Push/Pop arrays will have a negative value index used as a sentinel for the | |
// number of indices necessary to deference the array to the desired level. | |
// Whenever one of these instructions happen, we will simulate this behavior | |
// with the following helper code. | |
// This will also attempt to fix the sentinel value, which should follow | |
// UndertaleInstruction.InstanceType. | |
void ConsumeUntilMinus(ref Stack<UndertaleInstruction> stack, UndertaleVariable vari) | |
{ | |
UndertaleInstruction inst; | |
while (stack.Count > 0) { | |
inst = stack.Pop(); | |
if (inst.Kind == UndertaleInstruction.Opcode.PushI) { | |
if ((inst.Type1 == UndertaleInstruction.DataType.Int16 && (Int16)inst.Value < 0) || | |
(inst.Type1 == UndertaleInstruction.DataType.Int32 && (Int32)inst.Value < 0)) { | |
inst.Value = (short)vari.InstanceType; | |
break; | |
} | |
} | |
} | |
} | |
bool codeUsesAnyOf(UndertaleCode code, List<String> locals) | |
{ | |
foreach (var inst in code.Instructions) { | |
var type = UndertaleInstruction.GetInstructionType(inst.Kind); | |
if (!(type == UndertaleInstruction.InstructionType.PushInstruction || type == UndertaleInstruction.InstructionType.PopInstruction)) | |
continue; | |
if (inst.Value != null) { | |
String varName = (inst.Value as UndertaleInstruction.Reference<UndertaleVariable>)?.Target.Name.Content; | |
if (locals.Contains(varName)) | |
return true; | |
} | |
} | |
return false; | |
} | |
/* | |
ALGORITHM START | |
*/ | |
// Ensure all necessary functions for fixup are declared. | |
//defineFunctionIfMissing("string_hash_to_newline"); | |
//defineFunctionIfMissing("@@NewGMLArray@@"); | |
//int _UTMT__argc_str = getStringOrCreate("_UTMT__argc"); | |
//int _UTMT__argv_str = getStringOrCreate("_UTMT__argv"); | |
Data.Functions.EnsureDefined("string_hash_to_newline", Data.Strings, false); | |
Data.Functions.EnsureDefined("@@NewGMLArray@@", Data.Strings, false); | |
Data.Variables.EnsureDefined("argument0", UndertaleInstruction.InstanceType.Self, false, Data.Strings, Data); | |
Data.Variables.EnsureDefined("argument1", UndertaleInstruction.InstanceType.Self, false, Data.Strings, Data); | |
//var _UTMT__argc = Data.Variables.EnsureDefined("_UTMT__argc", UndertaleInstruction.InstanceType.Self, false, Data.Strings, Data); | |
//var _UTMT__argv = Data.Variables.EnsureDefined("_UTMT__argv", UndertaleInstruction.InstanceType.Self, false, Data.Strings, Data); | |
// Search for functions that might require argument fixup | |
var localArr = new List<String>{"argument", "argument_count"}; | |
foreach (var code in Data.Code) { | |
var script = Data.Scripts.FirstOrDefault(x => x.Code.Name.Content == code.Name.Content); | |
var scriptName = (script != null) ? script.Name.Content : code.Name.Content; | |
if (codeUsesAnyOf(code, localArr)) { | |
needsArgumentPatchingList.Add(scriptName); | |
} | |
} | |
// Go through all the game code | |
foreach (var code in Data.Code) { | |
List<FixUpType> FixUps = new List<FixUpType>(); | |
Stack<UndertaleInstruction> stack = new Stack<UndertaleInstruction>(); | |
UndertaleVariable _UTMT__argc = null; | |
UndertaleVariable _UTMT__argv = null; | |
var script = Data.Scripts.FirstOrDefault(x => x.Code.Name.Content == code.Name.Content); | |
var scriptName = (script != null) ? script.Name.Content : code.Name.Content; | |
if (needsArgumentPatchingList.Contains(scriptName)) { | |
// Create the necessary Locals | |
var originalReferencedLocalVars = code.FindReferencedLocalVars(); | |
var locals = Data.CodeLocals.ByName(code.Name.Content); | |
int count = locals.Locals.Count; | |
_UTMT__argc = Data.Variables.DefineLocal(originalReferencedLocalVars, count++, "_UTMT__argc", Data.Strings, Data); | |
_UTMT__argv = Data.Variables.DefineLocal(originalReferencedLocalVars, count++, "_UTMT__argv", Data.Strings, Data); | |
locals.Locals.Add(new UndertaleCodeLocals.LocalVar() { Index = (uint)_UTMT__argc.VarID, Name = _UTMT__argc.Name }); | |
locals.Locals.Add(new UndertaleCodeLocals.LocalVar() { Index = (uint)_UTMT__argv.VarID, Name = _UTMT__argv.Name }); | |
// Insert the compatibility preamble | |
FixUps.Add(new FixUpType() { | |
where = 0, | |
patch = | |
$"pushbltn.v self.argument0\n" + | |
$"pop.v.v local._UTMT__argc\n" + | |
$"pushbltn.v self.argument1\n" + | |
$"pop.v.v local._UTMT__argv" | |
}); | |
// Change the function signature | |
code.ArgumentsCount = 2; | |
} | |
// Recall last instruction to facilitate debugging... | |
UndertaleInstruction lastInst = null; | |
try { | |
// Simulate the stack, while also looking for necessary fixups. | |
foreach (var inst in code.Instructions) { | |
var instType = UndertaleInstruction.GetInstructionType(inst.Kind); | |
lastInst = inst; | |
switch(inst.Kind) { | |
case UndertaleInstruction.Opcode.Conv: | |
stack.Pop(); | |
stack.Push(inst); | |
break; | |
case UndertaleInstruction.Opcode.Mul: | |
case UndertaleInstruction.Opcode.Div: | |
case UndertaleInstruction.Opcode.Rem: | |
case UndertaleInstruction.Opcode.Mod: | |
case UndertaleInstruction.Opcode.Add: | |
case UndertaleInstruction.Opcode.Sub: | |
case UndertaleInstruction.Opcode.And: | |
case UndertaleInstruction.Opcode.Or: | |
case UndertaleInstruction.Opcode.Xor: | |
stack.Pop(); | |
stack.Pop(); | |
stack.Push(inst); | |
break; | |
case UndertaleInstruction.Opcode.Neg: | |
case UndertaleInstruction.Opcode.Not: | |
stack.Pop(); | |
stack.Push(inst); | |
break; | |
case UndertaleInstruction.Opcode.Shl: | |
case UndertaleInstruction.Opcode.Shr: | |
case UndertaleInstruction.Opcode.Cmp: | |
stack.Pop(); | |
stack.Pop(); | |
stack.Push(inst); | |
break; | |
case UndertaleInstruction.Opcode.Dup: | |
stack.Push(inst); | |
break; | |
case UndertaleInstruction.Opcode.Ret: | |
case UndertaleInstruction.Opcode.Exit: | |
break; | |
case UndertaleInstruction.Opcode.Popz: | |
stack.Pop(); | |
break; | |
case UndertaleInstruction.Opcode.B: | |
break; | |
case UndertaleInstruction.Opcode.Bt: | |
case UndertaleInstruction.Opcode.Bf: | |
stack.Pop(); | |
break; | |
case UndertaleInstruction.Opcode.PushEnv: | |
case UndertaleInstruction.Opcode.PopEnv: | |
break; | |
case UndertaleInstruction.Opcode.PushGlb: | |
case UndertaleInstruction.Opcode.Push: | |
case UndertaleInstruction.Opcode.PushLoc: | |
case UndertaleInstruction.Opcode.PushBltn: | |
case UndertaleInstruction.Opcode.PushI: | |
case UndertaleInstruction.Opcode.Pop: | |
// When pushing or popping array dereferences, we need to pop indices until we reach a sentinel value | |
if (inst.Type1 == UndertaleInstruction.DataType.Variable) { | |
var vari = inst.Value as UndertaleInstruction.Reference<UndertaleVariable>; | |
if (vari != null) { | |
if (vari.Target.Name.Content == "argument") vari.Target = _UTMT__argv; | |
if (vari.Target.Name.Content == "argument_count") vari.Target = _UTMT__argc; | |
} | |
if (inst.Value != null && ((inst.Value as UndertaleInstruction.Reference<UndertaleVariable>).Type == UndertaleInstruction.VariableType.Array)) { | |
ConsumeUntilMinus(ref stack, vari.Target); | |
} | |
} | |
if (inst.Kind == UndertaleInstruction.Opcode.Pop) | |
stack.Pop(); | |
else | |
stack.Push(inst); | |
break; | |
case UndertaleInstruction.Opcode.Call: | |
case UndertaleInstruction.Opcode.CallV: | |
PopAndRegisterFixups(inst, ref stack, ref FixUps, code); | |
stack.Push(inst); | |
break; | |
case UndertaleInstruction.Opcode.Break: | |
break; | |
default: | |
throw new Exception("No."); | |
} | |
} | |
// Perform all the necessary fixups detected | |
int fixNo = 1; | |
foreach (var fixup in FixUps) { | |
fixNo += insertCodeAt(fixup.patch, code, fixup.where + fixNo); | |
} | |
} | |
catch (Exception e) { | |
throw new Exception($"In {code.Name}, {code.Instructions.IndexOf(lastInst)}, {lastInst.ToString()}: \n{e.ToString()}"); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
this was for 2.3.x, but it didnt work out.