From f8e7bd37397c5c7f7896458cda2aa273e4df691c Mon Sep 17 00:00:00 2001
From: spherallic <spherallic@gmail.com>
Date: Fri, 15 Sep 2023 21:28:20 +0200
Subject: [PATCH] Add Lua parser for custom objects

---
 Source/Core/Builder.csproj                |   2 +
 Source/Core/Config/ScriptConfiguration.cs |   4 +-
 Source/Core/Data/DataManager.cs           |  71 ++++++++-
 Source/Core/Data/DataReader.cs            |   3 +
 Source/Core/Data/PK3StructuredReader.cs   |  29 ++++
 Source/Core/Data/WADReader.cs             |   8 +
 Source/Core/SRB2/LuaMobjStructure.cs      | 141 +++++++++++++++++
 Source/Core/SRB2/LuaParser.cs             | 176 ++++++++++++++++++++++
 Source/Core/ZDoom/ActorStructure.cs       |   5 +-
 Source/Core/ZDoom/ZDTextParser.cs         |   4 +-
 10 files changed, 436 insertions(+), 7 deletions(-)
 create mode 100644 Source/Core/SRB2/LuaMobjStructure.cs
 create mode 100644 Source/Core/SRB2/LuaParser.cs

diff --git a/Source/Core/Builder.csproj b/Source/Core/Builder.csproj
index 7e1443f44..e97ceaaf3 100644
--- a/Source/Core/Builder.csproj
+++ b/Source/Core/Builder.csproj
@@ -312,6 +312,8 @@
     <Compile Include="Rendering\Vector4.cs" />
     <Compile Include="Rendering\VertexBuffer.cs" />
     <Compile Include="Rendering\VisualSlopeHandle.cs" />
+    <Compile Include="SRB2\LuaMobjStructure.cs" />
+    <Compile Include="SRB2\LuaParser.cs" />
     <Compile Include="VisualModes\VisualSlope.cs" />
     <Compile Include="Windows\ILinedefEditForm.cs" />
     <Compile Include="Windows\ISectorEditForm.cs" />
diff --git a/Source/Core/Config/ScriptConfiguration.cs b/Source/Core/Config/ScriptConfiguration.cs
index 9a753dd9d..98f2501e6 100755
--- a/Source/Core/Config/ScriptConfiguration.cs
+++ b/Source/Core/Config/ScriptConfiguration.cs
@@ -58,7 +58,9 @@ namespace CodeImp.DoomBuilder.Config
 		KEYCONF,
 		FONTDEFS,
         ZSCRIPT,
-		DECALDEF
+		DECALDEF,
+		//SOC,
+		LUA
 	}
 	
 	public class ScriptConfiguration : IComparable<ScriptConfiguration>
diff --git a/Source/Core/Data/DataManager.cs b/Source/Core/Data/DataManager.cs
index a0e148503..5e8281c66 100755
--- a/Source/Core/Data/DataManager.cs
+++ b/Source/Core/Data/DataManager.cs
@@ -133,6 +133,7 @@ namespace CodeImp.DoomBuilder.Data
 		// Things combined with things created from Decorate
 		private DecorateParser decorate;
         private ZScriptParser zscript;
+		private LuaParser lua;
         private Dictionary<string, ActorStructure> zdoomclasses;
 		private List<ThingCategory> thingcategories;
 		private Dictionary<int, ThingTypeInfo> thingtypes;
@@ -460,6 +461,7 @@ namespace CodeImp.DoomBuilder.Data
 			LoadDehackedThings();
             LoadZScriptThings();
             LoadDecorateThings();
+			LoadLuaThings();
 			ApplyDehackedThings();
 			FixRenamedDehackedSprites();
             int thingcount = ApplyZDoomThings(spawnnums, doomednums);
@@ -1844,8 +1846,42 @@ namespace CodeImp.DoomBuilder.Data
 			}
 		}
 
-        // [ZZ] this retrieves ZDoom actor structure by class name.
-        public ActorStructure GetZDoomActor(string classname)
+		// sphere: This loads things from SRB2 Lua files.
+		private void LoadLuaThings()
+		{
+			// Create new parser
+			lua = new LuaParser();
+
+			// Go for all opened containers
+			foreach (DataReader dr in containers)
+			{
+				// Load Lua info cumulatively (the last Lua is added to the previous)
+				// I'm not sure if this is the right thing to do though.
+				currentreader = dr;
+				foreach (TextResourceData data in dr.GetLuaData())
+				{
+					// Parse the data
+					data.Stream.Seek(0, SeekOrigin.Begin);
+					lua.Parse(data, true);
+
+					//mxd. DECORATE lumps are interdepandable. Can't carry on...
+					if (lua.HasError)
+					{
+						lua.LogError();
+						break;
+					}
+				}
+			}
+
+			//mxd. Add to text resources collection
+			currentreader = null;
+
+			if (lua.HasError)
+				lua.ClearActors();
+		}
+
+		// [ZZ] this retrieves ZDoom actor structure by class name.
+		public ActorStructure GetZDoomActor(string classname)
         {
             classname = classname.ToLowerInvariant();
             ActorStructure outv;
@@ -1887,6 +1923,37 @@ namespace CodeImp.DoomBuilder.Data
             Dictionary<string, ActorStructure> mergedAllActorsByClass = decorate.AllActorsByClass.Concat(zscript.AllActorsByClass.Where(x => !decorate.AllActorsByClass.ContainsKey(x.Key))).ToDictionary(k => k.Key, v => v.Value);
             zdoomclasses = mergedAllActorsByClass;
 
+			// Parse SRB2 Lua/SOC objects
+			IEnumerable<ActorStructure> mobjs = lua.Mobjs; // lua.Mobjs.Union(soc.Mobjs);
+
+			foreach (ActorStructure actor in mobjs)
+			{
+				// Check if we want to add this actor
+				if (actor.DoomEdNum > 0)
+				{
+					// Check if we can find this thing in our existing collection
+					if (thingtypes.ContainsKey(actor.DoomEdNum))
+					{
+						// Update the thing
+						thingtypes[actor.DoomEdNum].ModifyByDecorateActor(actor);
+					}
+					else
+					{
+						// Find the category to put the actor in
+						ThingCategory cat = GetThingCategory(null, thingcategories, GetCategoryInfo(actor)); //mxd
+
+						// Add new thing
+						ThingTypeInfo t = new ThingTypeInfo(cat, actor); ;
+
+						cat.AddThing(t);
+						thingtypes.Add(t.Index, t);
+					}
+
+					// Count
+					counter++;
+				}
+			}
+
 			// Dictionary of replaced actors that have to be recategorized
 			Dictionary<int, ActorStructure> recategorizeactors = new Dictionary<int, ActorStructure>();
 
diff --git a/Source/Core/Data/DataReader.cs b/Source/Core/Data/DataReader.cs
index 932ec8c02..8ea8d866a 100755
--- a/Source/Core/Data/DataReader.cs
+++ b/Source/Core/Data/DataReader.cs
@@ -260,6 +260,9 @@ namespace CodeImp.DoomBuilder.Data
 		// When implemented, this returns the list of IWAD infos
 		public abstract List<IWadInfo> GetIWadInfos();
 
+		// When implemented, this returns Lua lumps
+		public abstract IEnumerable<TextResourceData> GetLuaData();
+
 		#endregion
 
 		#region ================== Load/Save (mxd)
diff --git a/Source/Core/Data/PK3StructuredReader.cs b/Source/Core/Data/PK3StructuredReader.cs
index e3320dc56..68fce0308 100755
--- a/Source/Core/Data/PK3StructuredReader.cs
+++ b/Source/Core/Data/PK3StructuredReader.cs
@@ -766,6 +766,35 @@ namespace CodeImp.DoomBuilder.Data
 
 		#endregion
 
+		#region ================== Lua
+
+		// sphere
+		public override IEnumerable<TextResourceData> GetLuaData()
+		{
+			// Error when suspended
+			if (issuspended) throw new Exception("Data reader is suspended");
+
+			List<TextResourceData> result = new List<TextResourceData>();
+
+			List<string> files = new List<string>();
+
+			// Can be several entries
+			files.AddRange(GetAllFilesWhichTitleStartsWith("", "LUA_", true));
+			files.AddRange(GetFilesWithExt("", "lua", true));
+
+			// Add to collection
+			foreach (string s in files)
+				result.Add(new TextResourceData(this, LoadFile(s), s, true));
+
+			// Find in any of the wad files
+			foreach (WADReader wr in wads) result.AddRange(wr.GetLuaData());
+
+			return result;
+		}
+
+		#endregion
+
+
 		#region ================== Generic text lumps loading (mxd)
 
 		public override IEnumerable<TextResourceData> GetTextLumpData(ScriptType scripttype, bool singular, bool partialtitlematch)
diff --git a/Source/Core/Data/WADReader.cs b/Source/Core/Data/WADReader.cs
index 47d06605c..624fda838 100755
--- a/Source/Core/Data/WADReader.cs
+++ b/Source/Core/Data/WADReader.cs
@@ -1135,6 +1135,14 @@ namespace CodeImp.DoomBuilder.Data
 			return result;
 		}
 
+		// sphere
+		public override IEnumerable<TextResourceData> GetLuaData()
+		{
+			if (issuspended) throw new Exception("Data reader is suspended");
+
+			return GetAllLumpsData("LUA_");
+		}
+
 		//mxd
 		public override IEnumerable<TextResourceData> GetTextLumpData(ScriptType scripttype, bool singular, bool ignored)
 		{
diff --git a/Source/Core/SRB2/LuaMobjStructure.cs b/Source/Core/SRB2/LuaMobjStructure.cs
new file mode 100644
index 000000000..34993e776
--- /dev/null
+++ b/Source/Core/SRB2/LuaMobjStructure.cs
@@ -0,0 +1,141 @@
+using CodeImp.DoomBuilder.Config;
+using CodeImp.DoomBuilder.Data;
+using CodeImp.DoomBuilder.Types;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Text;
+
+namespace CodeImp.DoomBuilder.ZDoom
+{
+
+    public sealed class LuaMobjStructure : ActorStructure
+	{
+        #region ================== DECORATE Actor Structure parsing
+
+        internal LuaMobjStructure(ZDTextParser zdparser, string objname)
+        {
+			classname = string.Empty;
+
+            LuaParser parser = (LuaParser)zdparser;
+            bool done = false; //mxd
+			General.WriteLogLine(objname);
+
+			if (string.IsNullOrEmpty(objname))
+            {
+                parser.ReportError("Expected actor class name");
+                return;
+            }
+
+			// Now parse the contents of actor structure
+
+			while (parser.SkipWhitespace(true))
+			{
+				string token = parser.ReadToken();
+				token = token.ToLowerInvariant();
+
+				switch (token)
+				{
+					case "}":
+						// Actor scope ends here, break out of this parse loop
+						done = true;
+						break;
+
+					// Property
+					default:
+						General.WriteLogLine(token);
+						// Property begins with $? Then the whole line is a single value
+						if (token.Contains("$"))
+						{
+							bool isflag = false;
+							bool isname = false;
+
+							switch (token)
+							{
+								case "$name":
+									token = "$title";
+									isname = true;
+									break;
+								case "$wallsprite":
+								case "$flatsprite":
+								case "$spawnceiling":
+									isflag = true;
+									break;
+							}
+							// This is for editor-only properties such as $sprite and $category
+							string t = token;
+							if (isflag)
+							{
+								parser.SkipWhitespace(false);
+								string val = parser.ReadLine();
+								flags[t.Substring(1)] = (val == "true" || val == "1") ? true : false;
+							}
+							else
+								props[token] = new List<string> { (parser.SkipWhitespace(false) ? parser.ReadLine() : "") };
+
+							if (isname)
+							{
+								General.WriteLogLine("name = " + props["$title"][0]);
+							}
+						}
+						else
+						{
+							string tokenname = token;
+							parser.SkipWhitespace(true);
+							token = parser.ReadToken();
+
+							if (token != "=")
+							{
+								parser.ReportError("Invalid Lua object parameter definition, missing =");
+								return;
+							}
+
+							parser.SkipWhitespace(true);
+							token = parser.ReadToken();
+
+							if (!token.EndsWith(","))
+							{
+								done = true;
+							}
+
+							List<string> values = new List<string>() { token.TrimEnd(new char[] { ',' }) };
+
+							//mxd. Translate scale to xscale and yscale
+							switch (tokenname)
+							{
+								case "scale":
+									props["xscale"] = values;
+									props["yscale"] = values;
+									break;
+								case "doomednum":
+									doomednum = int.Parse(values[0]);
+									General.WriteLogLine("parsed doomednum: " + DoomEdNum);
+									goto default;
+								case "height":
+								case "radius":
+									props[tokenname] = new List<string>() { GetNumbers(values[0]).ToString() };
+									break;
+								default:
+									props[tokenname] = values;
+									break;
+							}
+						}
+						break;
+				}
+
+				if (done) break; //mxd
+			}
+
+			// parsing done, process thing arguments
+			ParseCustomArguments();
+		}
+
+		private static string GetNumbers(string input)
+		{
+			return new string(input.Where(c => char.IsDigit(c)).ToArray());
+		}
+
+		#endregion
+	}
+}
diff --git a/Source/Core/SRB2/LuaParser.cs b/Source/Core/SRB2/LuaParser.cs
new file mode 100644
index 000000000..c2b842644
--- /dev/null
+++ b/Source/Core/SRB2/LuaParser.cs
@@ -0,0 +1,176 @@
+
+#region ================== Copyright (c) 2007 Pascal vd Heiden
+
+/*
+ * Copyright (c) 2007 Pascal vd Heiden, www.codeimp.com
+ * This program is released under GNU General Public License
+ * 
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ * 
+ */
+
+#endregion
+
+#region ================== Namespaces
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using CodeImp.DoomBuilder.Config;
+using CodeImp.DoomBuilder.Data;
+using CodeImp.DoomBuilder.Types;
+
+#endregion
+
+namespace CodeImp.DoomBuilder.ZDoom
+{
+	public sealed class LuaParser : ZDTextParser
+	{
+		#region ================== Delegates
+
+		public delegate void IncludeDelegate(LuaParser parser, string includefile);
+		
+		public IncludeDelegate OnInclude;
+
+		#endregion
+		
+		#region ================== Constants
+		
+		#endregion
+		
+		#region ================== Variables
+
+		//mxd. Script type
+		internal override ScriptType ScriptType { get { return ScriptType.LUA; } }
+
+		// SRB2 mobjs
+		private Dictionary<int, ActorStructure> mobjs;
+
+		//mxd. Includes tracking
+		private HashSet<string> parsedlumps;
+
+		//mxd. Disposing. Is that really needed?..
+		private bool isdisposed;
+
+		//
+		public bool NoWarnings = false;
+
+		#endregion
+
+		#region ================== Properties
+
+		/// <summary>
+		/// All mobjs that are supported by the current game.
+		/// </summary>
+		public ICollection<ActorStructure> Mobjs { get { return mobjs.Values; } }
+
+		#endregion
+
+		#region ================== Constructor / Disposer
+
+		// Constructor
+		public LuaParser()
+		{
+			// Syntax
+			whitespace = "\n \t\r\u00A0"; //mxd. non-breaking space is also space :)
+			specialtokens = "=\n";
+			skipregions = false; //mxd
+
+            ClearActors();
+		}
+		
+		// Disposer
+		public void Dispose()
+		{
+			mobjs = null;
+
+			isdisposed = true;
+		}
+
+		#endregion
+
+		#region ================== Parsing
+
+		protected internal override void LogWarning(string message, int linenumber)
+		{
+			if (NoWarnings)
+				return;
+			base.LogWarning(message, linenumber);
+		}
+
+		// This parses the given Lua stream
+		// Returns false on errors
+		public override bool Parse(TextResourceData data, bool clearerrors)
+		{
+			//mxd. Already parsed?
+			if(!base.AddTextResource(data))
+			{
+				if(clearerrors) ClearError();
+				return true;
+			}
+
+			// Cannot process?
+			if(!base.Parse(data, clearerrors)) return false;
+
+			// Continue until at the end of the stream
+			while (SkipWhitespace(true))
+			{
+				// Read a token
+				string token = ReadToken();
+				if (!string.IsNullOrEmpty(token))
+				{
+					if (!token.StartsWith("mobjinfo[") || !token.EndsWith("]")) continue;
+					string objname = token.Substring(9).TrimEnd(new char[] { ']' });
+
+					SkipWhitespace(true);
+					token = ReadToken();
+					if (token != "=")
+					{
+						continue;
+					}
+
+					SkipWhitespace(true);
+					token = ReadToken();
+					if (token != "{")
+					{
+						ReportError("Invalid object definition, missing {");
+						return false;
+					}
+					// Read actor structure
+					ActorStructure mobj = new LuaMobjStructure(this, objname);
+					if (mobj.props.ContainsKey("doomednum"))
+						General.WriteLogLine("Mobj doomednum = " + mobj.props["doomednum"][0]);
+					if (this.HasError) return false;
+
+					mobjs[mobj.DoomEdNum] = mobj;
+				}
+			}
+
+
+			// Return true when no errors occurred
+			return (ErrorDescription == null);
+		}
+		
+		#endregion
+		
+		#region ================== Methods
+
+		public ActorStructure GetMobjByDoomEdNum(int doomednum)
+		{
+			return mobjs.ContainsKey(doomednum) ? mobjs[doomednum] : null;
+		}
+
+        internal void ClearActors()
+        {
+            // Initialize
+			mobjs = new Dictionary<int, ActorStructure>();
+			parsedlumps = new HashSet<string>(StringComparer.OrdinalIgnoreCase); //mxd
+		}
+
+		#endregion
+	}
+}
diff --git a/Source/Core/ZDoom/ActorStructure.cs b/Source/Core/ZDoom/ActorStructure.cs
index 16e88aa5a..abd14ad27 100755
--- a/Source/Core/ZDoom/ActorStructure.cs
+++ b/Source/Core/ZDoom/ActorStructure.cs
@@ -22,6 +22,7 @@ using System.Globalization;
 using CodeImp.DoomBuilder.Config;
 using CodeImp.DoomBuilder.Data;
 using CodeImp.DoomBuilder.Types;
+using CodeImp.DoomBuilder.Map;
 
 #endregion
 
@@ -60,7 +61,7 @@ namespace CodeImp.DoomBuilder.ZDoom
         internal DecorateCategoryInfo catinfo;
 
         // [ZZ] direct ArgumentInfos (from game configuration), or own ArgumentInfos (from props)
-        internal ArgumentInfo[] args = new ArgumentInfo[5];
+        internal ArgumentInfo[] args = new ArgumentInfo[Thing.NUM_ARGS];
 
         // States
         internal Dictionary<string, StateStructure> states;
@@ -404,7 +405,7 @@ namespace CodeImp.DoomBuilder.ZDoom
         /// </summary>
         public void ParseCustomArguments()
         {
-            for (int i = 0; i < 5; i++)
+            for (int i = 0; i < Thing.NUM_ARGS; i++)
             {
                 if (HasProperty("$arg" + i))
                     args[i] = new ArgumentInfo(this, i);
diff --git a/Source/Core/ZDoom/ZDTextParser.cs b/Source/Core/ZDoom/ZDTextParser.cs
index cbd920eb4..1e106dab4 100755
--- a/Source/Core/ZDoom/ZDTextParser.cs
+++ b/Source/Core/ZDoom/ZDTextParser.cs
@@ -240,11 +240,11 @@ namespace CodeImp.DoomBuilder.ZDoom
 				c = (char)datareader.ReadByte();
 
 				// Check if this is comment
-				if(c == '/')
+				if (c == '/' || c == '-') // SRB2 Lua allows -- as comment syntax
 				{
 					if(datastream.Position == datastream.Length) return false;
 					char c2 = (char)datareader.ReadByte();
-					if(c2 == '/')
+					if(c2 == c)
 					{
 						// Check if not a special comment with a token
 						if(datastream.Position == datastream.Length) return false;
-- 
GitLab