Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions builder/HookshotBuilder/BuilderSettings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
namespace HookshotBuilder;

/// What the player chose last time (the install folder, the game files, the shaders' tick): builder-settings.json beside the versions
sealed class BuilderSettings
{
public string? Out { get; set; }
public string? Nup { get; set; }
public string? Romfs { get; set; }
public string? Exefs { get; set; }
public bool Dlss { get; set; } // (NVIDIA DLSS ticked: NVIDIA's license was accepted)
public bool PrepareLater { get; set; } // ("Prepare shaders now" unticked)

public static BuilderSettings? Load(string path)
{
try { return System.Text.Json.JsonSerializer.Deserialize<BuilderSettings>(System.IO.File.ReadAllText(path)); }
catch (Exception) { return null; }
}

/// (the updates, from the launcher's Update and from the --update start after a self-update, begin before ApplySettings sets
/// the tick from these settings: they read the saved choice itself. None saved: prepared, as the tick's default)
public static bool PrepareNow(string path) => Load(path)?.PrepareLater != true;
}
3 changes: 3 additions & 0 deletions builder/HookshotBuilder/MainWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,9 @@
</Style>
</ComboBox.FocusVisualStyle>
</ComboBox>
<!-- (make_game.py's last step, the game's prepare-shaders run: unticked, the game prepares them at its first start) -->
<CheckBox x:Name="PrepareTick" Style="{StaticResource Tick}" Content="Prepare shaders now" IsChecked="True" Margin="0,12,0,0"
ToolTip="Prepares the game's shaders at the end of the build, a few minutes (longer on some GPUs). Unticked, the game prepares them at its first start, behind a black window."/>
</StackPanel>
</Grid>

Expand Down
83 changes: 61 additions & 22 deletions builder/HookshotBuilder/MainWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -252,23 +252,15 @@ void LaunchPlayClick(object sender, RoutedEventArgs e)
void LaunchFolderClick(object sender, RoutedEventArgs e) =>
Process.Start(new ProcessStartInfo(launchFolder) { UseShellExecute = true });

// ---- what the player chose last time (the install folder, the game files): builder-settings.json beside the versions ----
sealed class BuilderSettings
{
public string? Out { get; set; }
public string? Nup { get; set; }
public string? Romfs { get; set; }
public string? Exefs { get; set; }
public bool Dlss { get; set; } // (NVIDIA DLSS ticked: NVIDIA's license was accepted)
}
// ---- what the player chose last time (BuilderSettings.cs): builder-settings.json beside the versions ----
static string SettingsPath => System.IO.Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "HookshotBuilder", "builder-settings.json");

void ApplySettings()
{
BuilderSettings? s = null;
try { s = System.Text.Json.JsonSerializer.Deserialize<BuilderSettings>(File.ReadAllText(SettingsPath)); } catch (Exception) { }
var s = BuilderSettings.Load(SettingsPath);
if (s == null) return;
PrepareTick.IsChecked = !s.PrepareLater;
if (!string.IsNullOrEmpty(s.Out)) OutPath.Text = s.Out;
DlssTick.IsChecked = s.Dlss;
if (s.Romfs != null && s.Exefs != null && Directory.Exists(s.Romfs) && File.Exists(System.IO.Path.Combine(s.Exefs, "main")))
Expand All @@ -293,7 +285,8 @@ void SaveSettings()
try
{
var s = new BuilderSettings { Out = OutPath.Text, Nup = romfs == null ? chosenNup : null, Romfs = romfs, Exefs = exefs,
Dlss = DlssTick.IsChecked == true };
Dlss = DlssTick.IsChecked == true,
PrepareLater = PrepareTick.IsChecked != true };
Directory.CreateDirectory(System.IO.Path.GetDirectoryName(SettingsPath)!);
File.WriteAllText(SettingsPath, System.Text.Json.JsonSerializer.Serialize(s));
}
Expand All @@ -306,7 +299,7 @@ void Preview(string what)
switch (what)
{
case "build":
MakeRows(update: false);
MakeRows(update: false, shaders: true);
SetStep(1);
ShowPage(BuildPage);
rows["read"].Begin(); rows["read"].Done(); rows["read"].Small("Version checked");
Expand Down Expand Up @@ -692,7 +685,8 @@ async void StartBuild(object sender, RoutedEventArgs e)
else if (chosenNup != null) args.AddRange(new[] { "--nup", chosenNup });
else return;
args.AddRange(new[] { "--out", OutPath.Text, "--language", lang.Code });
Run(tools, args, dlss: DlssTick.IsChecked == true);
prepareNow = PrepareTick.IsChecked == true;
Run(tools, ShaderStep.Args(args, prepareNow), dlss: DlssTick.IsChecked == true);
}

const string DlssLicense =
Expand All @@ -719,17 +713,30 @@ static readonly (string Id, string Build, string Update)[] Stages =
("lift", "Translate the game to PC code", "Translate the game to PC code"),
("compile", "Compile", "Compile changed files"),
("finish", "Put the game together", "Swap in the new game"),
(ShaderStep.Id, "Prepare the shaders", "Prepare the shaders"),
};
static readonly Dictionary<string, (double From, double Size)> Weight = new()
static readonly Dictionary<string, (double From, double Size)> WithShaders = new()
{
["download"] = (0, 0.04), ["read"] = (0.04, 0.04), ["lift"] = (0.08, 0.14), ["compile"] = (0.22, 0.68), ["finish"] = (0.90, 0.02),
[ShaderStep.Id] = (0.92, 0.08), // (hookshot --prepare-shaders: minutes on some GPUs, the first start's wait moved here)
};
static readonly Dictionary<string, (double From, double Size)> WithoutShaders = new()
{
["download"] = (0, 0.04), ["read"] = (0.04, 0.04), ["lift"] = (0.08, 0.14), ["compile"] = (0.22, 0.74), ["finish"] = (0.96, 0.04),
};
Dictionary<string, (double From, double Size)> Weight = WithoutShaders;
// (this build prepares the shaders: the tick for a build, the saved choice for an update. The game's @@stage shaders came, and
// make_game's line when it left them to the first start: the row's end says which)
bool prepareNow, shadersBegan;
string? shadersLater;
TimeSpan? builtIn; // (at @@times: the build without the shaders' minutes, as build_times.json keeps it)

void MakeRows(bool update)
void MakeRows(bool update, bool shaders)
{
StageList.Children.Clear();
rows.Clear();
var list = Stages.Where(s => update || s.Id != "download").ToList();
Weight = shaders ? WithShaders : WithoutShaders;
var list = Stages.Where(s => (update || s.Id != "download") && (shaders || s.Id != ShaderStep.Id)).ToList();
for (var i = 0; i < list.Count; i++)
{
var row = new StepRow(i + 1, update ? list[i].Update : list[i].Build, last: i == list.Count - 1);
Expand All @@ -752,7 +759,7 @@ void Run(Tools tools, List<string> args, bool keepRows = false, bool dlss = fals
if (!keepRows)
{
updating = false;
MakeRows(update: false);
MakeRows(update: false, shaders: prepareNow);
SetStep(1);
BuildTitle.Text = "Building your game";
UpdateBar.Visibility = Visibility.Collapsed;
Expand All @@ -769,6 +776,9 @@ void Run(Tools tools, List<string> args, bool keepRows = false, bool dlss = fals
compileEta = -1;
thisCompile = 0;
previousTotal = -1;
shadersBegan = false;
shadersLater = null;
builtIn = null;
ShowPage(BuildPage);
CancelButton.Visibility = Visibility.Visible;
SetProgress(updating ? Weight["read"].From : 0);
Expand All @@ -795,6 +805,7 @@ void Line(string line)
current = id;
if (rows.TryGetValue(id, out var row)) row.Begin();
if (id == "compile") compileClock.Restart();
if (id == ShaderStep.Id) shadersBegan = true;
if (Weight.TryGetValue(id, out var w)) SetProgress(w.From);
}
else if (line.StartsWith("@@progress "))
Expand All @@ -813,6 +824,7 @@ void Line(string line)
return;
}
else if (line.StartsWith("@@done ")) finished = line[7..];
else if (ShaderStep.Reason(line) is { } later) shadersLater = later;
else if (FilesLine.Match(line) is { Success: true } m) filesOf = int.Parse(m.Groups[1].Value);
else if (FileLine.IsMatch(line)) filesDone++;
if (current == "compile" && filesChanged < 0 && filesOf > 0 && rows.TryGetValue("compile", out var c))
Expand Down Expand Up @@ -843,6 +855,7 @@ void Number(string line)
compileEta = eta;
break;
case "@@times" when p.Length == 4:
builtIn = elapsed.Elapsed;
long.TryParse(p[2], out thisCompile);
long.TryParse(p[3], out previousTotal);
break;
Expand Down Expand Up @@ -916,7 +929,12 @@ void Ended(string? failure)
runner = null;
if (finished.Length > 0)
{
foreach (var r in rows.Values) r.Done();
foreach (var (id, r) in rows)
{
if (id != ShaderStep.Id) r.Done();
else if (ShaderStep.Skipped(shadersBegan, shadersLater) is { } why) { r.Skipped(); r.Small(why); }
else r.Done();
}
SetProgress(1);
ShowReady();
return;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A Cancel during the optional shaders step goes through the "cancelled" branch just below, back to the files page, even though the new game is already installed by then. So ShowReady never runs: no shortcuts, and the launcher doesn't learn the folder is ready. On an AMD GPU where preparation takes 20+ minutes, pressing Cancel there is quite likely. Linux has the same issue (on_exit shows "The build stopped"). Ending a cancel that comes after the finish stage as "ready, shaders left to the first start" would fit the step being optional.

Expand Down Expand Up @@ -957,7 +975,7 @@ void ShowReady()
SetStep(2);
ReadyTitle.Text = updating ? "Your game is updated" : "Your game is ready";
var compiledNote = thisCompile > 0 ? $" (compiled in {Span(TimeSpan.FromSeconds(thisCompile))})" : "";
ReadyNote.Text = $"{(updating ? "Updated" : "Built")} in {Span(elapsed.Elapsed)}{compiledNote}{Against(elapsed.Elapsed)} · {finished}";
ReadyNote.Text = $"{(updating ? "Updated" : "Built")} in {Span(elapsed.Elapsed)}{compiledNote}{Against(builtIn ?? elapsed.Elapsed)} · {finished}";
if (!updating && Environment.GetEnvironmentVariable("HOOKSHOT_UI_PREVIEW") == null)
Finder.Shortcuts(finished, DesktopTick.IsChecked == true, StartTick.IsChecked == true);
else if (updating && Environment.GetEnvironmentVariable("HOOKSHOT_UI_PREVIEW") == null)
Expand Down Expand Up @@ -1132,7 +1150,8 @@ public void BeginUpdate(string folder, string? tag)
StepPill.Visibility = Visibility.Collapsed;
VersionChip.Visibility = Visibility.Visible;
VersionChipText.Text = from != null && from != to ? $"{from} → {to}" : to;
MakeRows(update: true);
prepareNow = BuilderSettings.PrepareNow(SettingsPath);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This reads the saved choice correctly, but on the launcher path ApplySettings still never runs, so PrepareTick keeps its default (ticked). If the player then goes Back and presses Build in that window, SaveSettings writes PrepareLater = false over their saved true, and every later update prepares again. Loading the tick from prepareNow here (or calling ApplySettings on that path) would keep them in sync.

MakeRows(update: true, shaders: prepareNow);
BuildTitle.Text = "Updating your game";
BuildNote.Text = "Getting the update…";
RingBox.Visibility = Visibility.Collapsed;
Expand Down Expand Up @@ -1173,7 +1192,9 @@ internal void UpdateBuild(Tools tools, string folder)
// (a folder built with NVIDIA DLSS, its license accepted then, is built with it again)
var dlss = File.Exists(System.IO.Path.Combine(folder, "ngx", "nvngx_dlss.dll")) &&
File.Exists(System.IO.Path.Combine(Storage.NgxSdk, "LICENSE.txt"));
Run(tools, new List<string> { "--update", folder }, keepRows: true, dlss: dlss);
// (prepareNow: the saved choice, read in BeginUpdate. The launcher's Update and the --update start after a self-update never
// ran ApplySettings, so the tick there is only its default)
Run(tools, ShaderStep.Args(new List<string> { "--update", folder }, prepareNow), keepRows: true, dlss: dlss);
}

// ================= the window =================
Expand Down Expand Up @@ -1288,6 +1309,24 @@ public void Done()
time.Foreground = B("InkFaint");
}

/// (not done: left to later, the shaders' step when they were not prepared now)
public void Skipped()
{
if (running) Tick();
running = false;
watch.Stop();
disc.Fill = Brushes.Transparent;
disc.Stroke = B("GlassLineStrong");
disc.StrokeThickness = 1.5;
halo.Fill = Brushes.Transparent;
number.Text = "–";
number.Foreground = B("InkFaint");
tick.Visibility = Visibility.Collapsed;
name.Foreground = B("InkFaint");
name.FontWeight = FontWeights.Medium;
time.Foreground = B("InkFaint");
}

public void Stopped()
{
running = false;
Expand Down
30 changes: 30 additions & 0 deletions builder/HookshotBuilder/ShaderStep.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using System.Text.RegularExpressions;

namespace HookshotBuilder;

/// The build's last step, the shaders' preparation (nx2/make_game.py prepare_shaders): the argument that leaves it to the game's
/// first start, and what its row says once the build is done
static class ShaderStep
{
public const string Id = "shaders";
public const string Later = "--no-prepare-shaders";
// (make_game.py NOT_PREPARED: a plain line, so a builder that does not know it only shows it)
static readonly Regex NotPrepared = new(@"^the shaders were not prepared now \((.*)\): ");

/// make_game's arguments with the player's choice (unticked: the game prepares its shaders at its first start)
public static List<string> Args(List<string> args, bool prepare)
{
if (!prepare) args.Add(Later);
return args;
}

/// Why make_game left the shaders to the first start, when this line says so; else null
public static string? Reason(string line) => NotPrepared.Match(line) is { Success: true } m ? m.Groups[1].Value : null;

/// The line under the row of a finished build when the shaders were not prepared, or null when they were (a check mark).
/// began: the game's @@stage shaders came; reason: make_game's not-prepared line
public static string? Skipped(bool began, string? reason) =>
reason != null ? $"Left to the game's first start: {reason}"
: began ? null
: "Left to the game's first start";
}
27 changes: 19 additions & 8 deletions builder/linux/hookshot_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@

CONFIG = os.path.join(os.environ.get("XDG_CONFIG_HOME") or os.path.expanduser("~/.config"), "hookshot", "builder.ini")
STAGES = {"read": "Reading your game files", "lift": "Translating the game to PC code", "compile": "Compiling",
"finish": "Putting the game folder together"}
"finish": "Putting the game folder together", "shaders": "Preparing shaders"}


def left_text(seconds):
Expand Down Expand Up @@ -70,8 +70,7 @@ def play(folder):
exe = linux_update.game_exe(folder)
if not exe:
return
env = {k: v for k, v in os.environ.items() if not k.startswith(("HOOKSHOT_", "PYTHON", "APPDIR", "APPIMAGE", "ARGV0", "OWD"))}
env.pop("LD_LIBRARY_PATH", None)
env = make_game.game_env() # (as make_game's shader preparation and hookshot-launch: the game's own SDL, the system's libraries)
env["HOOKSHOT_NO_UPDATE_CHECK"] = "1"
subprocess.Popen([exe], cwd=folder, env=env, start_new_session=True,
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
Expand Down Expand Up @@ -178,6 +177,7 @@ def __init__(self, launch=None, update=None, fetched=False, tag=None):
names = [n for _, n in make_game.LANGUAGES]
code = self.cfg.get("language", "en-US")
self.language = tk.StringVar(value=dict(make_game.LANGUAGES).get(code, names[0]))
self.prepare = tk.BooleanVar(value=self.cfg.get("prepare", "1") != "0") # (make_game.py's last step: off, at the first start)
self.check_text = tk.StringVar()
self.space_text = tk.StringVar()
self.stage_text = tk.StringVar()
Expand Down Expand Up @@ -245,6 +245,9 @@ def show_form(self):
ttk.Combobox(f, textvariable=self.language, values=[n for _, n in make_game.LANGUAGES], state="readonly").grid(
row=row, column=1, sticky="w", **self.pad)
row += 1
ttk.Checkbutton(f, text="Prepare shaders now (unticked: at the game's first start, behind a black window)",
variable=self.prepare).grid(row=row, column=0, columnspan=3, sticky="w", **self.pad)
row += 1
ttk.Label(f, text="Takes about 45 minutes and needs 60 GB.").grid(row=row, column=0, columnspan=3, sticky="w", **self.pad)
row += 1
ttk.Label(f, textvariable=self.space_text).grid(row=row, column=0, columnspan=3, sticky="w", **self.pad)
Expand Down Expand Up @@ -414,7 +417,8 @@ def start_make_update(self, folder):
self.status.set("This builder is the latest version.")
self.lookup_latest()
return
cmd = [sys.executable, "-I", os.path.join(ROOT, "nx2", "make_game.py"), "--update", folder]
cmd = [sys.executable, "-I", os.path.join(ROOT, "nx2", "make_game.py"), "--update", folder] + \
([] if self.prepare.get() else ["--no-prepare-shaders"]) # (as the last build chose)
self.heading.configure(text="Updating your game")
self.stage_text.set("Compiling what changed")
self.started = time.time()
Expand Down Expand Up @@ -458,12 +462,13 @@ def build(self):
except ValueError as error:
return messagebox.showerror("Hookshot", str(error))
code = next((c for c, n in make_game.LANGUAGES if n == self.language.get()), "en-US")
self.cfg.update(kind=self.kind.get(), nup=self.nup.get(), romfs=self.romfs.get(), exefs=self.exefs.get(), out=out, language=code)
self.cfg.update(kind=self.kind.get(), nup=self.nup.get(), romfs=self.romfs.get(), exefs=self.exefs.get(), out=out, language=code,
prepare="1" if self.prepare.get() else "0")
save_config(self.cfg)
# (issue 531) Without HOOKSHOT_JOBS or RX_JOBS, make_game chooses as on Windows, from the memory free now; this window chose
# fewer, 80% of the total memory at 3.5 GB each, and the CPU sat at 29%
cmd = [sys.executable, "-I", os.path.join(ROOT, "nx2", "make_game.py")] + src + ["--out", out, "--language", code] + \
([] if jobs is None else ["--jobs", str(jobs)])
([] if jobs is None else ["--jobs", str(jobs)]) + ([] if self.prepare.get() else ["--no-prepare-shaders"])
self.show_progress()
self.started = time.time()
self.proc = subprocess.Popen(cmd, cwd=ROOT, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1,
Expand All @@ -482,6 +487,7 @@ def show_progress(self, title="Building"):
self.eta = None
self.compile_started = None
self.times = None
self.shaders_later = False
f = self.page = ttk.Frame(self)
f.pack(fill="both", expand=True, pady=self.px(8))
f.columnconfigure(0, weight=1)
Expand Down Expand Up @@ -547,8 +553,9 @@ def on_line(self, line):
stage = line.split(" ", 2)[1]
self.stage_text.set(STAGES.get(stage, line.split(" ", 2)[-1]))
self.bar.configure(mode="determinate", value=0)
if stage == "compile":
self.compile_started = time.time()
# (the compile's clock and countdown are the compile's: a later step, the minutes-long shaders one, without them)
self.compile_started = time.time() if stage == "compile" else None
self.eta = None
elif line.startswith("@@progress "):
try:
self.bar.configure(value=float(line.split()[1]))
Expand Down Expand Up @@ -591,6 +598,8 @@ def on_line(self, line):
self.built = line[7:]
elif line.startswith("@@error "):
self.error = line[8:]
elif line.startswith(make_game.NOT_PREPARED):
self.shaders_later = True
if hasattr(self, "log"):
self.log.insert("end", line + "\n")
self.log.see("end")
Expand All @@ -608,6 +617,8 @@ def on_exit(self, code):
text += ", compiled in %d:%02d" % (int(compiled) // 60, int(compiled) % 60)
if previous > 0:
text += " · last time %d:%02d" % (int(previous) // 3600, int(previous) // 60 % 60)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor: on Linux "built in" is wall time including the preparation, while "last time" is the @@times total, which now leaves it out. So a build that took exactly as long as last time reads 10 minutes slower when preparation took 10. Windows uses builtIn for this; comparing against self.times[0] here would match it.

if self.shaders_later:
text += " · the shaders are prepared at its first start"
self.stage_text.set(text)
self.bar.configure(value=1.0)
try:
Expand Down
Loading
Loading