Skip to content

Commit 85e7f46

Browse files
authored
Merge pull request #96 from Newbster-2/master
Scripting helper functions, compiler internal globals, better error reporting
2 parents 621042d + 505a78a commit 85e7f46

9 files changed

Lines changed: 446 additions & 163 deletions

File tree

TagTool/Commands/Scenarios/CompileScriptsCommand.cs

Lines changed: 123 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,40 +21,49 @@ public CompileScriptsCommand(GameCache cache, Scenario definition) :
2121
"CompileScripts",
2222
"Compile scripts from a file. (still may have a few issues)",
2323

24-
"CompileScripts <input_file> [append]",
24+
"CompileScripts <input_file> [append] [adjust_indexes]",
2525

2626
"Compiles HaloScript from a file and writes it to the scenario.\n" +
2727
"Use append to add to the existing scripts instead of replacing them.\n" +
2828
"When appending, the new file can freely call any script or global\n" +
29-
"already present in the scenario without re-declaring them.")
29+
"already present in the scenario without re-declaring them.\n" +
30+
"Use adjust_indexes to fix up script indexes in squad and objective\n" +
31+
"blocks after compilation (use when scripts are added or removed).")
3032
{
3133
Cache = cache;
3234
Definition = definition;
3335
}
3436

3537
public override object Execute(List<string> args)
3638
{
37-
if (args.Count < 1 || args.Count > 2)
39+
if (args.Count < 1 || args.Count > 3)
3840
return new TagToolError(CommandError.ArgCount);
3941

4042
bool append = false;
41-
string filePath;
43+
bool adjustIndexes = false;
4244

43-
filePath = args[0];
45+
string filePath = args[0];
4446

45-
if (args.Count == 2)
47+
for (int i = 1; i < args.Count; i++)
4648
{
47-
if (args[1] != "append")
48-
return new TagToolError(CommandError.ArgInvalid, $"Unknown flag '{args[1]}'. Did you mean append?");
49-
50-
append = true;
49+
switch (args[i])
50+
{
51+
case "append": append = true; break;
52+
case "adjust_indexes": adjustIndexes = true; break;
53+
default:
54+
return new TagToolError(CommandError.ArgInvalid, $"Unknown flag '{args[i]}'.");
55+
}
5156
}
5257

5358
var srcFile = new FileInfo(filePath);
54-
5559
if (!srcFile.Exists)
5660
return new TagToolError(CommandError.FileNotFound, $"\"{filePath}\"");
5761

62+
// Before compiling, snapshot script name -> old index so we can remap later.
63+
Dictionary<string, int> oldIndexByName = null;
64+
if (adjustIndexes)
65+
oldIndexByName = BuildScriptNameMap(Definition);
66+
5867
var scriptCompiler = new ScriptCompiler(Cache, Definition);
5968

6069
try
@@ -76,8 +85,111 @@ public override object Execute(List<string> args)
7685
return new TagToolError(CommandError.OperationFailed, $"Unexpected compiler error: {e.Message}");
7786
}
7887

88+
if (adjustIndexes)
89+
{
90+
var newIndexByName = BuildScriptNameMap(Definition);
91+
AdjustScriptIndexes(Definition, oldIndexByName, newIndexByName);
92+
Console.WriteLine("Script indexes adjusted.");
93+
}
94+
7995
Console.WriteLine(append ? "Done. Scripts appended." : "Done.");
8096
return true;
8197
}
98+
99+
// Builds a name -> index map from the scenario's current Scripts list.
100+
private static Dictionary<string, int> BuildScriptNameMap(Scenario definition)
101+
{
102+
var map = new Dictionary<string, int>(StringComparer.Ordinal);
103+
for (int i = 0; i < definition.Scripts.Count; i++)
104+
map[definition.Scripts[i].ScriptName] = i;
105+
return map;
106+
}
107+
108+
// Remaps a single script index field using the old and new name->index maps.
109+
// If the index was -1 (none) or out of range, it is left unchanged.
110+
private static short Remap(short oldIndex, IReadOnlyList<HsScript> oldScripts,
111+
Dictionary<string, int> newIndexByName)
112+
{
113+
if (oldIndex < 0 || oldIndex >= oldScripts.Count)
114+
return oldIndex;
115+
116+
string name = oldScripts[oldIndex].ScriptName;
117+
return newIndexByName.TryGetValue(name, out int newIdx) ? (short)newIdx : oldIndex;
118+
}
119+
120+
private static void AdjustScriptIndexes(Scenario definition,
121+
Dictionary<string, int> oldIndexByName,
122+
Dictionary<string, int> newIndexByName)
123+
{
124+
// Reconstruct the old scripts list by index so we can look up names.
125+
// We use the name map we captured before compilation; build a lookup list
126+
// from it for O(1) index -> name -> new index.
127+
var oldScripts = definition.Scripts; // post-compile list (same names, new indexes)
128+
129+
// Build old index -> name from the pre-compile snapshot.
130+
// Since we only stored name->index we invert it here.
131+
var oldNameByIndex = new Dictionary<int, string>();
132+
foreach (var kv in oldIndexByName)
133+
oldNameByIndex[kv.Value] = kv.Key;
134+
135+
// Helper that remaps using the pre/post name snapshots.
136+
short RemapIndex(short oldIndex)
137+
{
138+
if (oldIndex < 0 || !oldNameByIndex.TryGetValue(oldIndex, out string name))
139+
return oldIndex;
140+
return newIndexByName.TryGetValue(name, out int newIdx) ? (short)newIdx : oldIndex;
141+
}
142+
143+
// --- Squads ---
144+
if (definition.Squads != null)
145+
{
146+
foreach (var squad in definition.Squads)
147+
{
148+
// Fireteams (H3 style, MaxVersion Halo3Retail)
149+
if (squad.Fireteams != null)
150+
foreach (var ft in squad.Fireteams)
151+
ft.CommandScriptIndex = RemapIndex(ft.CommandScriptIndex);
152+
153+
// Designer fireteams (ODST+)
154+
if (squad.DesignerFireteams != null)
155+
foreach (var ft in squad.DesignerFireteams)
156+
ft.CommandScriptIndex = RemapIndex(ft.CommandScriptIndex);
157+
158+
// Templated fireteams (ODST+)
159+
if (squad.TemplatedFireteams != null)
160+
foreach (var ft in squad.TemplatedFireteams)
161+
ft.CommandScriptIndex = RemapIndex(ft.CommandScriptIndex);
162+
163+
// Spawn formations (ODST+)
164+
if (squad.SpawnFormations != null)
165+
foreach (var sf in squad.SpawnFormations)
166+
sf.PlacementScriptIndex = RemapIndex(sf.PlacementScriptIndex);
167+
168+
// Spawn points (ODST+)
169+
if (squad.SpawnPoints != null)
170+
foreach (var sp in squad.SpawnPoints)
171+
sp.PlacementScriptIndex = RemapIndex(sp.PlacementScriptIndex);
172+
}
173+
}
174+
175+
// --- AI Objectives -> Tasks ---
176+
if (definition.AiObjectives != null)
177+
{
178+
foreach (var objective in definition.AiObjectives)
179+
{
180+
if (objective.Tasks == null)
181+
continue;
182+
183+
foreach (var task in objective.Tasks)
184+
{
185+
task.EntryScriptIndex = RemapIndex(task.EntryScriptIndex);
186+
task.CommandScriptIndex = RemapIndex(task.CommandScriptIndex);
187+
task.ExhaustionScriptIndex = RemapIndex(task.ExhaustionScriptIndex);
188+
task.ScriptIndex = RemapIndex(task.ScriptIndex);
189+
}
190+
}
191+
}
192+
}
82193
}
83194
}
195+
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using System.Text;
5+
using System.Text.RegularExpressions;
6+
using TagTool.Cache;
7+
using TagTool.Commands.Common;
8+
using TagTool.Tags.Definitions;
9+
10+
namespace TagTool.Commands.Scenarios
11+
{
12+
class PatchScriptsCommand : Command
13+
{
14+
private GameCache Cache { get; }
15+
private Scenario Definition { get; }
16+
17+
public PatchScriptsCommand(GameCache cache, Scenario definition) :
18+
base(true,
19+
20+
"PatchScripts",
21+
"Patch a script file by substituting function names for compatibility.",
22+
23+
"PatchScripts <input_file> <output_file> [flags]",
24+
25+
"Performs string-level substitutions on an uncompiled script file to make it\n" +
26+
"compatible with this version of the game. Optionally accepts an integer flags\n" +
27+
"value to enable or disable specific substitution groups.")
28+
{
29+
Cache = cache;
30+
Definition = definition;
31+
}
32+
33+
public override object Execute(List<string> args)
34+
{
35+
if (args.Count < 2 || args.Count > 3)
36+
return new TagToolError(CommandError.ArgCount);
37+
38+
string inputPath = args[0];
39+
string outputPath = args[1];
40+
41+
int flags = ~0; // all flags set by default
42+
if (args.Count == 3)
43+
{
44+
if (!int.TryParse(args[2], out flags))
45+
return new TagToolError(CommandError.ArgInvalid, $"Flags must be an integer, got '{args[2]}'.");
46+
}
47+
48+
var inputFile = new FileInfo(inputPath);
49+
if (!inputFile.Exists)
50+
return new TagToolError(CommandError.FileNotFound, $"\"{inputPath}\"");
51+
52+
string source = File.ReadAllText(inputPath);
53+
string patched = ApplyPatches(source, flags);
54+
File.WriteAllText(outputPath, patched, new UTF8Encoding(false));
55+
56+
Console.WriteLine($"Done. Patched script written to \"{outputPath}\".");
57+
return true;
58+
}
59+
60+
// Each entry: (requiredFlags, pattern, replacement)
61+
// requiredFlags - bitmask checked against the flags arg; 0 means always apply
62+
// pattern - the exact function name (or multi-line block) to find
63+
// replacement - what to replace it with
64+
//
65+
// For simple one-to-one function renames, pattern and replacement are just the names.
66+
// For block substitutions, write them out in full including parens/whitespace.
67+
//
68+
// Simple rename example:
69+
// (0, "old_function_name", "new_function_name"),
70+
//
71+
// Block substitution example:
72+
// (0, "(old_function_x)", "(new_function_x)\n (new_function_y)"),
73+
private static readonly (int flags, string pattern, string replacement)[] Patches =
74+
[
75+
// h3 -> ed (odst) change
76+
(1, "cinematic_scripting_start_animation",
77+
"cinematic_scripting_start_animation_legacy"),
78+
79+
(1, "cinematic_scripting_create_object",
80+
"cinematic_scripting_create_object_legacy"),
81+
82+
(1, "cinematic_scripting_create_and_animate_object",
83+
"cinematic_scripting_create_and_animate_object_legacy"),
84+
85+
(1, "cinematic_scripting_create_and_animate_object_no_animation",
86+
"cinematic_scripting_create_and_animate_object_no_animation_legacy"),
87+
88+
(1, "cinematic_scripting_create_and_animate_cinematic_object",
89+
"cinematic_scripting_create_and_animate_cinematic_object_legacy"),
90+
91+
(1, "cinematic_scripting_destroy_object",
92+
"cinematic_scripting_destroy_object_legacy"),
93+
94+
// h3 -> odst change
95+
(1, "cinematic_object_get_unit",
96+
"cinematic_object_get"),
97+
98+
(1, "cinematic_object_get_scenery",
99+
"cinematic_object_get"),
100+
101+
(1, "cinematic_object_get_effect_scenery",
102+
"cinematic_object_get"),
103+
104+
// h3 -> odst change
105+
(1, "vehicle_test_seat_list",
106+
"vehicle_test_seat_unit_list"),
107+
108+
(1, "vehicle_test_seat",
109+
"vehicle_test_seat_unit"),
110+
111+
// h3 -> ed (odst) change
112+
(4, "vehicle_test_seat_list",
113+
"vehicle_test_seat_list_legacy"),
114+
115+
(4, "vehicle_test_seat",
116+
"vehicle_test_seat_legacy"),
117+
118+
// h3/osdt -> ms23 change
119+
(0, "player_action_test_cinematic_skip",
120+
"player_action_test_accept"),
121+
122+
(0, "player_action_test_start",
123+
"player_action_test_accept"),
124+
125+
];
126+
127+
private static string ApplyPatches(string source, int flags)
128+
{
129+
foreach (var (patchFlags, pattern, replacement) in Patches)
130+
{
131+
// skip if the required flags aren't set (0 means always apply)
132+
if (patchFlags != 0 && (flags & patchFlags) == 0)
133+
continue;
134+
135+
// For simple identifiers (no parens/newlines in the pattern), use a
136+
// word-boundary match so we don't clip substrings of longer names.
137+
// For block patterns, do a literal string replace.
138+
if (IsIdentifier(pattern))
139+
source = ReplaceIdentifier(source, pattern, replacement);
140+
else
141+
source = source.Replace(pattern, replacement);
142+
}
143+
144+
return source;
145+
}
146+
147+
// Replaces whole identifier occurrences only - won't match if the name is
148+
// immediately preceded or followed by another identifier character.
149+
private static string ReplaceIdentifier(string source, string name, string replacement)
150+
{
151+
// Identifier chars: letters, digits, underscore
152+
var sb = new StringBuilder(source.Length);
153+
int i = 0;
154+
while (i < source.Length)
155+
{
156+
if (source[i] == name[0] && source.Length - i >= name.Length &&
157+
source.Substring(i, name.Length) == name)
158+
{
159+
bool prevOk = i == 0 || !IsIdentifierChar(source[i - 1]);
160+
bool nextOk = i + name.Length == source.Length || !IsIdentifierChar(source[i + name.Length]);
161+
162+
if (prevOk && nextOk)
163+
{
164+
sb.Append(replacement);
165+
i += name.Length;
166+
continue;
167+
}
168+
}
169+
170+
sb.Append(source[i++]);
171+
}
172+
173+
return sb.ToString();
174+
}
175+
176+
private static bool IsIdentifier(string s)
177+
{
178+
foreach (char c in s)
179+
if (!IsIdentifierChar(c)) return false;
180+
return s.Length > 0;
181+
}
182+
183+
private static bool IsIdentifierChar(char c) =>
184+
char.IsLetterOrDigit(c) || c == '_';
185+
}
186+
}

TagTool/Commands/Scenarios/ScenarioContextFactory.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ public static void Populate(CommandContext context, GameCache cache, CachedTag t
2626
context.AddCommand(new CompileScriptsCommand(cache, scenario));
2727
context.AddCommand(new CompilePodiumScriptsCommand(cache, scenario));
2828
context.AddCommand(new ListScriptsCommand(cache, tag, scenario));
29+
context.AddCommand(new PatchScriptsCommand(cache, scenario));
2930
context.AddCommand(new ExtractZonesAreasModelCommand(cache, scenario));
3031

3132
if(cache is GameCacheHaloOnlineBase hoCache)

TagTool/Porting/Gen3/PortingContextGen3.Scripts.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -410,9 +410,9 @@ private void ConvertScriptExpressionData(Stream cacheStream, Stream blamCacheStr
410410
if (expr.Data[2] == 0xFF && expr.Data[3] == 0xFF)
411411
{
412412
var opcode = BitConverter.ToUInt16(expr.Data, 0) & ~0x8000;
413-
var name = BlamCache.ScriptDefinitions.Globals[opcode];
414-
var global = CacheContext.ScriptDefinitions.Globals.FirstOrDefault(p => p.Value == name);
415-
if (global.Value == null)
413+
var name = BlamCache.ScriptDefinitions.Globals[opcode].Name;
414+
var global = CacheContext.ScriptDefinitions.Globals.FirstOrDefault(p => p.Value.Name == name);
415+
if (global.Value.Name == null)
416416
{
417417
Log.Warning($"No equivalent script global was found for '{name}' (0x{expr.Opcode:X3}, expr {scnr.ScriptExpressions.IndexOf(expr)})");
418418
ConvertScriptExpressionUnsupportedOpcode(expr);

0 commit comments

Comments
 (0)