Skip to content

Commit

Permalink
[HS-1467] Support usage of 'require' in Lua; refactor several scripti…
Browse files Browse the repository at this point in the history
…ng things
  • Loading branch information
baughj committed Aug 4, 2024
1 parent d657031 commit d691409
Show file tree
Hide file tree
Showing 6 changed files with 148 additions and 91 deletions.
3 changes: 1 addition & 2 deletions hybrasyl/Objects/Monster.cs
Original file line number Diff line number Diff line change
Expand Up @@ -269,11 +269,10 @@ public void OnSpawn()
World.ScriptProcessor.RegisterScriptAttachment(damageScript, this);
if (!Script.HasFunction("OnSpawn"))
return;

Script.ExecuteFunction("OnSpawn", ScriptEnvironment.Create(("origin", this), ("source", this)));
}


public override void Damage(double damage, ElementType element = ElementType.None,
DamageType damageType = DamageType.Direct, DamageFlags damageFlags = DamageFlags.None,
Creature attacker = null, Castable castable = null, bool onDeath = true)
Expand Down
34 changes: 1 addition & 33 deletions hybrasyl/Servers/World.cs
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ public bool Init()
return false;
}

CompileScripts(); // We compile scripts first so that all future operations requiring scripts work
ScriptProcessor.CompileScripts(); // We compile scripts first so that all future operations requiring scripts work
if (!LoadData())
{
GameLog.Fatal("There were errors loading basic world data. Hybrasyl has halted.");
Expand Down Expand Up @@ -695,38 +695,6 @@ private void GenerateMetafiles()
#endregion
}

public void CompileScripts()
{
// Scan each directory for *.lua files
var numFiles = 0;
var numErrors = 0;
foreach (var file in Directory.GetFiles(ScriptDirectory, "*.lua", SearchOption.AllDirectories))
{
var path = file.Replace(ScriptDirectory, "");
var scriptname = Path.GetFileName(file);
if (path.StartsWith("_"))
continue;
GameLog.ScriptingInfo($"Loading script: {path}");
try
{
var script = new Script(file, ScriptProcessor);
ScriptProcessor.RegisterScript(script);
if (path.StartsWith("common"))
script.Run();
numFiles++;
}
catch (Exception e)
{
GameLog.ScriptingError($"Script {scriptname}: Registration failed: {e}");
numErrors++;
}
}

GameLog.Info($"Scripts: loaded {numFiles} scripts");
if (numErrors > 0)
GameLog.Error($"Scripts: {numErrors} scripts had errors - check scripting log");
}

public IMessageHandler ResolveMessagingPlugin(MessageType type, Message message)
{
// Do we have a plugin that would handle this message?
Expand Down
66 changes: 19 additions & 47 deletions hybrasyl/Subsystems/Scripting/Script.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
using Hybrasyl.Servers;
using Hybrasyl.Xml.Objects;
using MoonSharp.Interpreter;
using Serilog;
using System;
using System.IO;
using System.Linq;
Expand All @@ -31,69 +30,41 @@

namespace Hybrasyl.Subsystems.Scripting;

/// <summary>
/// A logging class that can be used by scripts natively in Lua.
/// </summary>
[MoonSharpUserData]
public class ScriptLogger
{
public ScriptLogger(string name)
{
ScriptName = name;
}

public string ScriptName { get; set; }

public void Info(string message)
{
Log.Information("{ScriptName} : {Message}", ScriptName, message);
}

public void Error(string message)
{
Log.Error("{ScriptName} : {Message}", ScriptName, message);
}
}

public class Script
public class Script(string path, ScriptProcessor processor)
{
private static readonly Regex LuaRegex = new(@"(.*):\(([0-9]*),([0-9]*)-([0-9]*)\): (.*)$");
public string RawSource { get; set; }
public string Name { get; set; }
public string FullPath { get; }
public string RawSource { get; set; } = string.Empty;
public string Name { get; set; } = Path.GetFileName((string) path).ToLower();
public string FullPath { get; } = path;
public string FileName => Path.GetFileName(FullPath);
public string Locator { get; set; } = string.Empty;
public ScriptProcessor Processor { get; set; }
public ScriptProcessor Processor { get; set; } = processor;
public MoonSharp.Interpreter.Script Compiled { get; private set; }
public bool Disabled { get; set; }
public ScriptExecutionResult LoadExecutionResult { get; set; }
public Guid Guid { get; set; } = Guid.Empty;

private object _lock = new();

public Script(string path, ScriptProcessor processor)
{
FullPath = path;
Name = Path.GetFileName(path).ToLower();
Compiled = new MoonSharp.Interpreter.Script(CoreModules.Preset_SoftSandbox);
RawSource = string.Empty;
Processor = processor;
}
private readonly object _lock = new();

public Script(string script, string name)
private void NewMoonsharpScript()
{
FullPath = string.Empty;
Name = name;
Compiled = new MoonSharp.Interpreter.Script(CoreModules.Preset_SoftSandbox);
RawSource = script;
Compiled = new MoonSharp.Interpreter.Script(CoreModules.GlobalConsts | CoreModules.TableIterators |
CoreModules.String | CoreModules.Table | CoreModules.Basic |
CoreModules.Math | CoreModules.Bit32 | CoreModules.LoadMethods)
{
Options =
{
ScriptLoader = new WorldModuleLoader(Processor.World.ScriptDirectory, "modules")
}
};
}

public ScriptExecutionResult Reload()
{
lock (_lock)
{
Disabled = true;
Compiled = new MoonSharp.Interpreter.Script(CoreModules.Preset_SoftSandbox);
NewMoonsharpScript();
return Run();
}
}
Expand Down Expand Up @@ -222,10 +193,11 @@ public ScriptExecutionResult Run(bool onLoad = true)
var result = new ScriptExecutionResult();
try
{
NewMoonsharpScript();
SetGlobals();
// Load file into RawSource so we have access to it later
RawSource = File.ReadAllText(FullPath);
result.Return = Compiled.DoFile(FullPath);
result.Return = Compiled.DoString(RawSource);
result.Result = ScriptResult.Success;
LoadExecutionResult = result;
if (!onLoad)
Expand Down
17 changes: 17 additions & 0 deletions hybrasyl/Subsystems/Scripting/ScriptLogger.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using Hybrasyl.Internals.Logging;
using MoonSharp.Interpreter;

namespace Hybrasyl.Subsystems.Scripting;

/// <summary>
/// A logging class that can be used by scripts natively in Lua.
/// </summary>
[MoonSharpUserData]
public class ScriptLogger(string name)
{
public string ScriptName { get; set; } = name;

public void Info(string message) => GameLog.ScriptingInfo($"script log: {ScriptName}: {message}");

public void Error(string message) => GameLog.ScriptingError($"script log: {ScriptName}: {message}");
}
54 changes: 45 additions & 9 deletions hybrasyl/Subsystems/Scripting/ScriptProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,23 @@
using MoonSharp.Interpreter;
using System;
using System.Collections.Generic;
using System.IO;

namespace Hybrasyl.Subsystems.Scripting;

public class ScriptProcessor(World world)
{
public World World { get; } = world;
private Dictionary<Guid, Script> _scripts = new();
private Dictionary<string, Guid> _locatorIndex = new();
private Dictionary<string, Guid> _nameIndex = new();
private Dictionary<Guid, List<Guid>> _scriptAttachments = new();

static ScriptProcessor()
{
// Register UserData types for MoonScript. This only needs to be done once.
// NB registering assemblies is required for RegisterType of
// any type in that assembly to work correctly
UserData.RegisterAssembly(typeof(Game).Assembly);
UserData.RegisterType<Gender>();
UserData.RegisterType<LegendIcon>();
Expand All @@ -46,17 +56,43 @@ static ScriptProcessor()
DynValue.NewString(v.ToString()));
}

// Register UserData types for MoonScript
// NB registering assemblies is required for RegisterType of
// any type in that assembly to work correctly
public void CompileScripts()
{
// Scan each directory for *.lua files
var numFiles = 0;
var numErrors = 0;
foreach (var file in Directory.GetFiles(World.ScriptDirectory, "*.lua", SearchOption.AllDirectories))
{
var path = file.Replace(World.ScriptDirectory, "");
var scriptName = Path.GetFileName(file);
if (path.StartsWith("_") || path.StartsWith("modules"))
continue;

try
{
var script = new Script(file, this);
RegisterScript(script);
if (path.StartsWith("common") || path.StartsWith("startup"))
{
GameLog.ScriptingInfo($"{nameof(CompileScripts)}: Loading & executing script {path}");
script.Run();
}
else
GameLog.ScriptingInfo($"{nameof(CompileScripts)}: Loading {path}");
numFiles++;
}
catch (Exception e)
{
GameLog.ScriptingError($"{nameof(CompileScripts)}: {scriptName}: Registration failed: {e}");
numErrors++;
}
}

public World World { get; } = world;
GameLog.Info($"{nameof(CompileScripts)}: loaded {numFiles} scripts");
if (numErrors > 0)
GameLog.Error($"{nameof(CompileScripts)}: {numErrors} scripts had errors - check scripting log");
}

private Dictionary<Guid, Script> _scripts = new();
private Dictionary<string, Guid> _locatorIndex = new();
private Dictionary<string, Guid> _nameIndex = new();
private Dictionary<Guid, List<Guid>> _scriptAttachments = new();

private string GenerateLocator(string path)
{
var locator = path.Replace(World.ScriptDirectory, "").Replace(@"\", ":").Replace("/", ":");
Expand Down
65 changes: 65 additions & 0 deletions hybrasyl/Subsystems/Scripting/WorldModuleLoader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
using MoonSharp.Interpreter;
using MoonSharp.Interpreter.Loaders;
using System;
using System.IO;

namespace Hybrasyl.Subsystems.Scripting;

/// <summary>
/// A Moonsharp script loader, used to enable "require" and "loadfile" in Lua
/// scripts to load includes (modules) from a blessed / safe path.
/// </summary>
/// <param name="includePath">The path to the allowed directory. </param>
public class WorldModuleLoader(string basePath, string modulePath) : ScriptLoaderBase
{
public string ModulePath { get; } = modulePath;
public string BasePath { get; } = basePath;

private string GetFullPathname(string moduleName) =>
Path.Join(Path.Join(BasePath, ModulePath), $"{moduleName}.lua");

/// <summary>
/// World module loader for "require" in Lua.
/// </summary>
/// <param name="modname">The module name passed from the script.</param>
/// <param name="globalContext">The script's global context</param>
/// <returns></returns>
public override string ResolveModuleName(string modname, Table globalContext)
{
if (string.IsNullOrEmpty(modname)) throw new ArgumentNullException(nameof(modname));

var pathName = GetFullPathname(modname);

if (Path.Exists(pathName)) return pathName;

throw new ArgumentException($"{nameof(ResolveModuleName)}: module {modname} does not exist");

}

/// <summary>
/// World module handler for "loadfile" in Lua. We disallow this usage except for module loading.
/// </summary>
/// <param name="file">The file name to load.</param>
/// <param name="globalContext">The script's global context.</param>
/// <returns></returns>
/// <exception cref="ArgumentException"></exception>
public override object LoadFile(string file, Table globalContext)
{
var d2 = new DirectoryInfo(file);
var d1 = new DirectoryInfo(Path.Join(BasePath, ModulePath));
var isParent = false;
while (d2.Parent != null)
{
if (d2.Parent.FullName == d1.FullName) { isParent = true; break; }
d2 = d2.Parent;
}

if (!isParent) throw new ArgumentException($"{nameof(LoadFile)}: module path loading outside of {d2.FullName} is prohibited");
return File.ReadAllText(file);
}

public override bool ScriptFileExists(string name)
{
throw new NotImplementedException("Use require instead");
}
}

0 comments on commit d691409

Please sign in to comment.