From 45dc796dca76b0aaabba027189cd587bc92cb660 Mon Sep 17 00:00:00 2001
From: biwa <6475593+biwa@users.noreply.github.com>
Date: Sat, 21 Aug 2021 20:42:06 +0200
Subject: [PATCH] ZScript: added support for extending classes. Resolves #597

---
 Source/Core/ZDoom/ZScriptParser.cs | 111 +++++++++++++++++++++++------
 1 file changed, 89 insertions(+), 22 deletions(-)

diff --git a/Source/Core/ZDoom/ZScriptParser.cs b/Source/Core/ZDoom/ZScriptParser.cs
index 3f3125c78..6d3f9565c 100755
--- a/Source/Core/ZDoom/ZScriptParser.cs
+++ b/Source/Core/ZDoom/ZScriptParser.cs
@@ -22,6 +22,8 @@ namespace CodeImp.DoomBuilder.ZDoom
             public ZScriptActorStructure Actor { get; internal set; }
             internal DecorateCategoryInfo Region;
 			public bool IsMixin { get; internal set; }
+			public bool IsExtension { get; internal set; }
+			public List<ZScriptClassStructure> Extensions { get; internal set; }
 
             // these are used for parsing and error reporting
             public ZScriptParser Parser { get; internal set; }
@@ -34,7 +36,7 @@ namespace CodeImp.DoomBuilder.ZDoom
             // textresourcepath
             public string TextResourcePath { get; internal set; }
 
-            internal ZScriptClassStructure(ZScriptParser parser, string classname, string replacesname, string parentname, bool ismixin, DecorateCategoryInfo region)
+            internal ZScriptClassStructure(ZScriptParser parser, string classname, string replacesname, string parentname, bool ismixin, bool isextension, DecorateCategoryInfo region)
             {
                 Parser = parser;
 
@@ -52,6 +54,8 @@ namespace CodeImp.DoomBuilder.ZDoom
                 Region = region;
 
 				IsMixin = ismixin;
+				IsExtension = isextension;
+				Extensions = new List<ZScriptClassStructure>();
             }
 
             internal void RestoreStreamData()
@@ -97,7 +101,7 @@ namespace CodeImp.DoomBuilder.ZDoom
                 if (ReplacementName != null) log_inherits += ((log_inherits.Length > 0) ? ", " : "") + "replaces " + ReplacementName;
                 if (log_inherits.Length > 0) log_inherits = " (" + log_inherits + ")";
 
-                if (isactor || IsMixin)
+                if (isactor || IsMixin || IsExtension)
                 {
                     Actor = new ZScriptActorStructure(Parser, Region, ClassName, ReplacementName, ParentName);
                     if (Parser.HasError)
@@ -106,26 +110,29 @@ namespace CodeImp.DoomBuilder.ZDoom
                         return false;
                     }
 
-                    // check actor replacement.
-                    Parser.archivedactors[Actor.ClassName.ToLowerInvariant()] = Actor;
-                    Parser.realarchivedactors[Actor.ClassName.ToLowerInvariant()] = Actor;
-                    if (Actor.CheckActorSupported())
-                        Parser.actors[Actor.ClassName.ToLowerInvariant()] = Actor;
-
-                    // Replace an actor?
-                    if (Actor.ReplacesClass != null)
-                    {
-                        if (Parser.GetArchivedActorByName(Actor.ReplacesClass, false) != null)
-                            Parser.archivedactors[Actor.ReplacesClass.ToLowerInvariant()] = Actor;
-                        else
-                            Parser.LogWarning("Unable to find \"" + Actor.ReplacesClass + "\" class to replace, while parsing \"" + Actor.ClassName + "\"");
+					if (!IsExtension)
+					{
+						// check actor replacement.
+						Parser.archivedactors[Actor.ClassName.ToLowerInvariant()] = Actor;
+						Parser.realarchivedactors[Actor.ClassName.ToLowerInvariant()] = Actor;
+						if (Actor.CheckActorSupported())
+							Parser.actors[Actor.ClassName.ToLowerInvariant()] = Actor;
+
+						// Replace an actor?
+						if (Actor.ReplacesClass != null)
+						{
+							if (Parser.GetArchivedActorByName(Actor.ReplacesClass, false) != null)
+								Parser.archivedactors[Actor.ReplacesClass.ToLowerInvariant()] = Actor;
+							else
+								Parser.LogWarning("Unable to find \"" + Actor.ReplacesClass + "\" class to replace, while parsing \"" + Actor.ClassName + "\"");
 
-                        if (Actor.CheckActorSupported() && Parser.GetActorByName(Actor.ReplacesClass) != null)
-                            Parser.actors[Actor.ReplacesClass.ToLowerInvariant()] = Actor;
-                    }
+							if (Actor.CheckActorSupported() && Parser.GetActorByName(Actor.ReplacesClass) != null)
+								Parser.actors[Actor.ReplacesClass.ToLowerInvariant()] = Actor;
+						}
 
-                    //mxd. Add to current text resource
-                    if (!Parser.scriptresources[TextResourcePath].Entries.Contains(Actor.ClassName)) Parser.scriptresources[TextResourcePath].Entries.Add(Actor.ClassName);
+						//mxd. Add to current text resource
+						if (!Parser.scriptresources[TextResourcePath].Entries.Contains(Actor.ClassName)) Parser.scriptresources[TextResourcePath].Entries.Add(Actor.ClassName);
+					}
                 }
 
                 //Parser.LogWarning(string.Format("Parsed {0}class {1}{2}", isactor?"actor ":"", ClassName, log_inherits));
@@ -750,7 +757,7 @@ namespace CodeImp.DoomBuilder.ZDoom
 			// now if we are a class, and we inherit actor, parse this entry as an actor. don't process extensions.
 			if (!isstruct && !extend && !mixin)
 			{
-				ZScriptClassStructure cls = new ZScriptClassStructure(this, tok_classname.Value, (tok_replacename != null) ? tok_replacename.Value : null, (tok_parentname != null) ? tok_parentname.Value : null, false, region);
+				ZScriptClassStructure cls = new ZScriptClassStructure(this, tok_classname.Value, (tok_replacename != null) ? tok_replacename.Value : null, (tok_parentname != null) ? tok_parentname.Value : null, false, false, region);
 				cls.Position = cpos;
 				string clskey = cls.ClassName.ToLowerInvariant();
 				if (allclasses.ContainsKey(clskey))
@@ -762,11 +769,32 @@ namespace CodeImp.DoomBuilder.ZDoom
 				allclasses.Add(cls.ClassName.ToLowerInvariant(), cls);
 				allclasseslist.Add(cls);
 			}
+			else if(!isstruct && extend)
+			{
+				string clskey = tok_classname.Value.ToLowerInvariant();
+
+				if(!allclasses.ContainsKey(clskey))
+				{
+					ReportError("Trying to extend class " + tok_classname.Value + " before it was defined");
+					return false;
+				}
+
+				// GZDoom doesn't allow extending classes that are from another archive
+				if(!allclasses[clskey].DataLocation.Equals(datalocation))
+				{
+					ReportError("Trying to extend class " + tok_classname.Value + " that's not part of data location \"" + datalocation + "\", but \"" + allclasses[clskey].DataLocation + "\"");
+					return false;
+				}
+
+				ZScriptClassStructure cls = new ZScriptClassStructure(this, tok_classname.Value, null, null, false, true, region);
+				cls.Position = cpos;
+				allclasses[clskey].Extensions.Add(cls);
+			}
 			else if (mixin)
 			{
 				// This is a bit ugly. We're treating mixin classes as actors, even though they aren't. But otherwise the parser
 				// doesn't parse all the actor info we need
-				ZScriptClassStructure cls = new ZScriptClassStructure(this, tok_classname.Value, null, null, true, region);
+				ZScriptClassStructure cls = new ZScriptClassStructure(this, tok_classname.Value, null, null, true, false, region);
 				cls.Position = cpos;
 				string clskey = cls.ClassName.ToLowerInvariant();
 				if(mixinclasses.ContainsKey(clskey))
@@ -987,6 +1015,11 @@ namespace CodeImp.DoomBuilder.ZDoom
             {
                 if (!cls.Process())
                     return false;
+
+				// Process extensions
+				foreach (ZScriptClassStructure extension in cls.Extensions)
+					if (!extension.Process())
+						return false;
             }
 
 			// Parse mixin class data
@@ -1113,6 +1146,40 @@ namespace CodeImp.DoomBuilder.ZDoom
 								actor.flags["solid"] = true;
 						}
 					}
+
+					// Extensions. https://zdoom.org/wiki/ZScript_classes#Extending_Classes
+					if (cls.Extensions.Count > 0)
+					{
+						foreach(ZScriptClassStructure extension in cls.Extensions)
+						{
+							ActorStructure extenseionactor = extension.Actor;
+
+							if (extenseionactor == null)
+								continue;
+
+							// States
+							if (extenseionactor.states.ContainsKey("spawn"))
+								actor.states["spawn"] = extenseionactor.GetState("spawn");
+
+							// Properties
+							if (extenseionactor.props.ContainsKey("height"))
+								actor.props["height"] = new List<string>(extenseionactor.props["height"]);
+
+							if (extenseionactor.props.ContainsKey("radius"))
+								actor.props["radius"] = new List<string>(extenseionactor.props["radius"]);
+
+							// Flags
+							if (extenseionactor.flags.ContainsKey("spawnceiling"))
+								actor.flags["spawnceiling"] = true;
+
+							if (extenseionactor.flags.ContainsKey("solid"))
+								actor.flags["solid"] = true;
+
+							// user_ variables
+							foreach (string uservarname in extenseionactor.uservars.Keys)
+								actor.uservars[uservarname] = extenseionactor.uservars[uservarname];
+						}
+					}
 				}
             }
 
-- 
GitLab