Skip to content

Commit

Permalink
refactor: Re-use Lua environment for entire Custom Info
Browse files Browse the repository at this point in the history
  • Loading branch information
psyGamer committed Feb 22, 2025
1 parent 6b1fa40 commit c04451b
Show file tree
Hide file tree
Showing 7 changed files with 153 additions and 151 deletions.
5 changes: 0 additions & 5 deletions CelesteTAS-EverestInterop/CelesteTAS-EverestInterop.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,6 @@
<PackageReference Include="JetBrains.Profiler.Api" Version="1.4.6" Condition="'$(Configuration)' == 'Debug' or '$(EnablePerformanceProfiling)' == 'true'" />
</ItemGroup>

<!-- Embedded Resources -->
<ItemGroup>
<EmbeddedResource Include="Source\Lua\environment.lua" LogicalName="environment.lua"/>
</ItemGroup>

<!-- Assemblies -->
<Target Name="CopyAssemblies" AfterTargets="Build">
<Copy SourceFiles="$(OutputPath)$(AssemblyName).dll" DestinationFolder="..\bin" />
Expand Down
14 changes: 7 additions & 7 deletions CelesteTAS-EverestInterop/Source/InfoHUD/GameInfo.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using Celeste;
using Celeste.Mod;
using JetBrains.Annotations;
using Microsoft.Xna.Framework;
using Monocle;
using StudioCommunication;
using StudioCommunication.Util;
Expand Down Expand Up @@ -182,9 +181,10 @@ private static void ResetCache() {
sessionData.Reset();

if (customInfoTemplateHash != TasSettings.InfoCustomTemplate.GetHashCode()) {
customInfoComponents.Reset();
customInfoTemplate.Reset();
}

InfoCustom.AdvanceValueStorage();
customInfo.Reset();
customInfoExact.Reset();
customInfoExactForceAllowCodeExecution.Reset();
Expand Down Expand Up @@ -224,7 +224,7 @@ private static void OnLevelTransition(Level level, LevelData next, Vector2 direc
private static LazyValue<(string RoomName, string ChapterTime)?> sessionData = new(QuerySessionData);

private static int customInfoTemplateHash = int.MaxValue;
private static LazyValue<InfoCustom.TemplateComponent[]> customInfoComponents = new(QueryCustomInfoComponents);
private static LazyValue<InfoCustom.Template> customInfoTemplate = new(ParseCustomInfoTemplate);

private static LazyValue<string> customInfo = new(QueryDisplayCustomInfo);
private static LazyValue<string> customInfoExact = new(QueryExactCustomInfo);
Expand Down Expand Up @@ -385,14 +385,14 @@ private static (string RoomName, string ChapterTime)? QuerySessionData() {
return null;
}

private static InfoCustom.TemplateComponent[] QueryCustomInfoComponents() {
private static InfoCustom.Template ParseCustomInfoTemplate() {
customInfoTemplateHash = TasSettings.InfoCustomTemplate.GetHashCode();
return InfoCustom.ParseTemplate(TasSettings.InfoCustomTemplate);
}

private static string QueryDisplayCustomInfo() => InfoCustom.EvaluateTemplate(customInfoComponents.Value, TasSettings.CustomInfoDecimals);
private static string QueryExactCustomInfo() => InfoCustom.EvaluateTemplate(customInfoComponents.Value, GameSettings.MaxDecimals);
private static string QueryExactCustomInfoForceAllowCodeExecution() => InfoCustom.EvaluateTemplate(customInfoComponents.Value, GameSettings.MaxDecimals, forceAllowCodeExecution: true);
private static string QueryDisplayCustomInfo() => InfoCustom.EvaluateTemplate(customInfoTemplate.Value, TasSettings.CustomInfoDecimals);
private static string QueryExactCustomInfo() => InfoCustom.EvaluateTemplate(customInfoTemplate.Value, GameSettings.MaxDecimals);
private static string QueryExactCustomInfoForceAllowCodeExecution() => InfoCustom.EvaluateTemplate(customInfoTemplate.Value, GameSettings.MaxDecimals, forceAllowCodeExecution: true);

private const string PositionPrefix = "Pos: ";
private const string SpeedPrefix = "Speed: ";
Expand Down
91 changes: 68 additions & 23 deletions CelesteTAS-EverestInterop/Source/InfoHUD/InfoCustom.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using JetBrains.Annotations;
using Microsoft.Xna.Framework;
using Monocle;
using StudioCommunication;
using System;
Expand Down Expand Up @@ -49,36 +48,61 @@ public static class InfoCustom {
/// Returns the parsed info for the current template
[Obsolete("Use InfoCustom.ParseTemplate() once and call InfoCustom.EvaluateTemplate() with the parsed components to render")]
public static IEnumerable<string> ParseTemplate(IEnumerable<string> template, int decimals, bool forceAllowCodeExecution = false) {
var components = ParseTemplate(string.Join('\n', template));
return [EvaluateTemplate(components, decimals, forceAllowCodeExecution)];
var parsedTemplate = ParseTemplate(string.Join('\n', template));
return [EvaluateTemplate(parsedTemplate, decimals, forceAllowCodeExecution)];
}

#region Parsing

public abstract record TemplateComponent;
/// Opaque handle for a parsed template
[PublicAPI]
public class Template : IDisposable {
internal TemplateComponent[] Components;

internal NeoLua.Lua Lua;
internal NeoLua.LuaTable Environment;

internal Template(TemplateComponent[] components, NeoLua.Lua lua, NeoLua.LuaTable environment) {
Components = components;

Lua = lua;
Environment = environment;
}
~Template() => Dispose();

public void Dispose() {
GC.SuppressFinalize(this);
Lua.Dispose();
}
}

internal abstract record TemplateComponent;

/// Raw text
private record TextComponent(string Text) : TemplateComponent;
/// Target-query
private record QueryComponent(TargetQuery.Parsed Query, string Prefix, ValueFormatter? Formatter = null) : TemplateComponent;
/// Lua code
private record LuaComponent(LuaContext Context) : TemplateComponent {
~LuaComponent() => Context.Dispose();
}
private record LuaComponent(NeoLua.LuaChunk Chunk) : TemplateComponent;
/// Table
private record TableComponent : TemplateComponent {
public int ComponentCount { get; set; } = 0;
}

/// Parses a custom Info HUD template into individual components, which can later be evaluated with <see cref="EvaluateTemplate"/>
[PublicAPI]
public static TemplateComponent[] ParseTemplate(string template) {
public static Template ParseTemplate(string template) {
List<TemplateComponent> components = [];
PopulateComponents(template, components);
var lua = new NeoLua.Lua();
var environment = lua.CreateEnvironment<LuaHelperEnvironment>();
environment.DefineFunction("stashValue", StashLuaValue);
environment.DefineFunction("restoreValue", RestoreLuaValue);

return components.ToArray();
PopulateComponents(template, components, lua);

static void PopulateComponents(string template, List<TemplateComponent> components) {
return new Template(components.ToArray(), lua, environment);

static void PopulateComponents(string template, List<TemplateComponent> components, NeoLua.Lua lua) {
Match? lastMatch = null;

while (true) {
Expand Down Expand Up @@ -140,18 +164,18 @@ static void PopulateComponents(string template, List<TemplateComponent> componen
} else if (currMatch == nextLuaMatch) {
string code = currMatch.Groups[1].Value;

var ctx = LuaContext.Compile(code, "CustomInfo");
if (ctx.Failure) {
components.Add(new TextComponent($"<Invalid Lua code: {ctx.Error}>"));
var chunkResult = LuaContext.CompileChunk(lua, code, "CustomInfo");
if (chunkResult.Failure) {
components.Add(new TextComponent($"<Invalid Lua code: {chunkResult.Error}>"));
} else {
components.Add(new LuaComponent(ctx));
components.Add(new LuaComponent(chunkResult));
}
} else if (currMatch == nextTableMatch) {
var table = new TableComponent();
components.Add(table);

int startComponentCount = components.Count + 1;
PopulateComponents(nextTableMatch.Groups[1].Value, components);
PopulateComponents(nextTableMatch.Groups[1].Value, components, lua);
table.ComponentCount = components.Count - startComponentCount;
}
}
Expand All @@ -171,12 +195,12 @@ static void PopulateComponents(string template, List<TemplateComponent> componen

/// Evaluates a parsed template into a string for the current values
[PublicAPI]
public static string EvaluateTemplate(TemplateComponent[] components, int decimals, bool forceAllowCodeExecution = false) {
public static string EvaluateTemplate(Template template, int decimals, bool forceAllowCodeExecution = false) {
infoBuilder.Clear();

// Format components
for (int i = 0; i < components.Length; i++) {
switch (components[i]) {
for (int i = 0; i < template.Components.Length; i++) {
switch (template.Components[i]) {
case TextComponent text: {
infoBuilder.Append(text.Text);
continue;
Expand Down Expand Up @@ -234,7 +258,7 @@ public static string EvaluateTemplate(TemplateComponent[] components, int decima
infoBuilder.Append("<Cannot safely evaluate Lua code during EnforceLegal>");
}

var result = lua.Context.Execute();
var result = LuaContext.ExecuteChunk(lua.Chunk, template.Environment);
if (result.Failure) {
infoBuilder.Append($"<Lua error: {result.Error.Message}>");
} else {
Expand Down Expand Up @@ -265,7 +289,7 @@ public static string EvaluateTemplate(TemplateComponent[] components, int decima
// Collect data
int endIdx = i + table.ComponentCount;
for (; i <= endIdx; i++) {
switch (components[i + 1]) {
switch (template.Components[i + 1]) {
case TextComponent text: {
resultBuilder.Append(text.Text);
continue;
Expand All @@ -275,7 +299,7 @@ public static string EvaluateTemplate(TemplateComponent[] components, int decima
resultBuilder.Append("<Cannot safely evaluate Lua code during EnforceLegal>");
}

var result = lua.Context.Execute();
var result = LuaContext.ExecuteChunk(lua.Chunk, template.Environment);
if (result.Failure) {
resultBuilder.Append($"<Lua error: {result.Error.Message}>");
} else {
Expand Down Expand Up @@ -502,6 +526,27 @@ private static string ColliderToString(Collider collider, int iterationHeight =
};
}

private static Dictionary<string, object> currValueStorage = new(), nextValueStorage = new();

/// Stashes the value under the specified key, to be restored on the next frame
private static void StashLuaValue(string key, object? value) {
if (value == null) {
return;
}

nextValueStorage[key] = value;
}
/// Restores a stashed value of the previous frame. Returns <c>null</c> if there is no value available
private static object? RestoreLuaValue(string key) {
return currValueStorage.GetValueOrDefault(key);
}

internal static void AdvanceValueStorage() {
// Swap for next frame
(currValueStorage, nextValueStorage) = (nextValueStorage, currValueStorage);
nextValueStorage.Clear();
}

#endregion

#if DEBUG || PROFILE
Expand All @@ -519,7 +564,7 @@ public static void CmdBenchmark(int iterCount = 100) {
const string luaTemplate = """
Float position: [[if not player then return 0 end; return math.floor((player.PositionRemainder.X*360+0.5)%360)]]/360, [[if not player then return 0 end; return math.floor(((player.Position.X+player.PositionRemainder.X)*360+0.5)%540)]]/540, [[if not player then return 0 end; oldfloat = float; subp = player.PositionRemainder.X*360%1;if subp < 0.5 then float = math.floor((subp)*3000000) else float = math.floor((subp-1)*3000000) end; return float]]/3mil
Air manip (65s): x - [[if not player then return 0 end; if level.Paused then return manip65 end; oldmanip65 = manip65; pos5 = math.floor((player.PositionRemainder.X*360+0.5)%5); manip65 = math.floor(((player.Position.x+player.PositionRemainder.X)*360%540*5-21*pos5+0.5)%108); val = manip65.."/108 (+"..pos5.."/5)"; return val]], Δx - [[if not player or not oldmanip65 then return 0 end; ans = manip65 - oldmanip65; return (ans-54)%108-54]] (+[[if not player then return 0 end; return math.floor((player.Speed.x/60%1*360+0.5)%5)]]/5)
Float speed: [[if not player then return 0 end; subp = player.Speed.x/60%1; return math.floor(subp*360+0.5)]]/360, [[if not player then return 0 end; subp = player.Speed.x/60%1*360%1;if subp < 0.5 then return math.floor((subp)*3000000) else return math.floor((subp-1)*3000000) end]]/3mil
Float speed: [[if not player then return 0 end; subp = player.Speed.X/60%1; return math.floor(subp*360+0.5)]]/360, [[if not player then return 0 end; subp = player.Speed.X/60%1*360%1;if subp < 0.5 then return math.floor((subp)*3000000) else return math.floor((subp-1)*3000000) end]]/3mil
Float velocity: [[if not player or not oldfloat then return 0 end; return float - oldfloat]]/3mil
""";
const string bothTemplate = $"""
Expand Down
49 changes: 29 additions & 20 deletions CelesteTAS-EverestInterop/Source/Lua/LuaContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@ namespace TAS.Lua;
/// Compiled Lua code which can be executed
internal readonly struct LuaContext : IDisposable {

private static string EnvironmentCode = null!;

[Load]
private static void Load() {
// Remove '.IsPublic' check to allow accessing non-public members
Expand All @@ -29,11 +27,6 @@ private static void Load() {
typeof(NeoLua.LuaType)
.GetMethodInfo("IsCallableMethod")!
.OnHook(bool (Func<MethodInfo, bool, bool> _, MethodInfo methodInfo, bool searchStatic) => (methodInfo.CallingConvention & CallingConventions.VarArgs) == 0 && methodInfo.IsStatic == searchStatic);

using var envStream = typeof(CelesteTasModule).Assembly.GetManifestResourceStream("environment.lua")!;
using var envReader = new StreamReader(envStream);

EnvironmentCode = envReader.ReadToEnd();
}

private readonly NeoLua.Lua lua;
Expand All @@ -43,36 +36,52 @@ private static void Load() {
private LuaContext(NeoLua.Lua lua, NeoLua.LuaChunk chunk, NeoLua.LuaTable environment) {
this.lua = lua;
this.chunk = chunk;

this.environment = environment;
}
public void Dispose() {
lua.Dispose();
}

/// Compiles Lua text code into an executable context
public static Result<LuaContext, string> Compile(string code, string? name = null) {
try {
var lua = new NeoLua.Lua();
var chunk = lua.CompileChunk(EnvironmentCode + code, name ?? "CelesteTAS_LuaContext", new NeoLua.LuaCompileOptions { DebugEngine = NeoLua.LuaExceptionDebugger.Default } );
var environment = lua.CreateEnvironment();
public static Result<LuaContext, string> Compile(string code, string name = "CelesteTAS_LuaContext") {
var lua = new NeoLua.Lua();
var chunkResult = CompileChunk(lua, code, name);
if (chunkResult.Failure) {
return Result<LuaContext, string>.Fail(chunkResult.Error);
}
var environment = lua.CreateEnvironment<LuaHelperEnvironment>();

return Result<LuaContext, string>.Ok(new LuaContext(lua, chunk, environment));
return Result<LuaContext, string>.Ok(new LuaContext(lua, chunkResult, environment));
}

/// Compiles Lua text code into an executable chunk
public static Result<NeoLua.LuaChunk, string> CompileChunk(NeoLua.Lua lua, string code, string name = "CelesteTAS_LuaContext") {
try {
var chunk = lua.CompileChunk(code, name, new NeoLua.LuaCompileOptions { DebugEngine = NeoLua.LuaExceptionDebugger.Default } );
return Result<NeoLua.LuaChunk, string>.Ok(chunk);
} catch (NeoLua.LuaException ex) {
ex.LogException("Lua compilation error");
return Result<LuaContext, string>.Fail(ex.Message); // Stacktrace isn't useful for the user
return Result<NeoLua.LuaChunk, string>.Fail(ex.Message); // Stacktrace isn't useful for the user
} catch (Exception ex) {
return Result<LuaContext, string>.Fail($"Unexpected error: {ex.Message}");
return Result<NeoLua.LuaChunk, string>.Fail($"Unexpected error: {ex.Message}");
}
}

/// Executes the compiled Lua code
public Result<IEnumerable<object?>, (string Message, string? Stacktrace)> Execute() {
public Result<IEnumerable<object?>, (string Message, string? Stacktrace)> Execute() => ExecuteChunk(chunk, environment);

/// Executes the compiled Lua code
public static Result<IEnumerable<object?>, (string Message, string? Stacktrace)> ExecuteChunk(NeoLua.LuaChunk chunk, NeoLua.LuaTable environment) {
try {
var result = chunk.Run(environment, null);
return Result<IEnumerable<object?>, (string Message, string? Stacktrace)>.Ok(
result.Values
// Level is an IEnumerable<Entity>, but we don't want to format it like that
.SelectMany(value => value is not Level && value is IEnumerable<object?> enumerable ? enumerable : [value]));

// Flatten returned collection
if (result.Count == 1 && result[0] != null && (result[0].GetType().IsArray || result[0].GetType().IsAssignableTo(typeof(IList)))) {
return Result<IEnumerable<object?>, (string Message, string? Stacktrace)>.Ok((IEnumerable<object?>) result[0]);
}

return Result<IEnumerable<object?>, (string Message, string? Stacktrace)>.Ok(result.Values);
} catch (NeoLua.LuaException ex) {
ex.LogException("Lua execution error");
return Result<IEnumerable<object?>, (string Message, string? Stacktrace)>.Fail((ex.Message, ex.StackTrace));
Expand Down
Loading

0 comments on commit c04451b

Please sign in to comment.