[ZScript] - Curious Rethink of Action Programming (C.R.A.P.)

Sun Sep 19, 2021 12:42 am

Ok, this is the last thread I'll make in the scripting forum for a while, I swear.

I've alluded to this in a few posts around here but I should just actually explain what I'm trying... The short version is I've been conducting unethical experiments into using ZScript in place of ACS for level element control and logic.

Why?

I dunno. Saving myself the mild annoyance of having to compile ACS by punching another system in the face to make it do something it was never intended to, I guess. Perfectly sensible. I already posted something of a previous incarnation of this, but that was a bit different as it was more of a module that can be dropped in with a specifically-defined function (in that case, an elevator controller). I definitely plan on making use of that side of it, but this is a more generalized approach that I may or may not make use of. It's mostly an experiment of curiosity.

I'm absolutely positive someone else has gone down this road before, and maybe made an open library for anyone to use, or possibly been thrown to the crocodiles. I didn't really look. Mostly I just want to see if I can make it work from my own knowledge and research, which, against the wishes of the kingdom of heaven, it seems to. I'm just kind of putting it out there to see what people think of the idea (and maybe hatemail, I don't know if there is a scripting language holy war or anything going on, I'm new here)

What I came up with to this end mainly consists of two actors, a ZLevelScript that gets placed in the map and is given the name of a class derived from ZLSInstance that is then spawned by it (so you don't need 200 different actors to pick from in the editor for each script). The ZLSInstance is the abstract base for any custom scripts, the ZLevelScript spits one out and keeps track of it, if the ZLSInstance is still extant, the script is considered to still be running, and the runner can't be re-activated. To activate it in the first place, generally you just bonk it with Thing_Activate from a line special (or an ACS script, if you want to get extra illegal), and it does its thing.

Technically sequencing things could be done in pretty much any way inside the Tick function, but I did it by just hijacking sprite states, since you can run line specials on them...

Code:
class Arena_Crusher : ZLSInstance
{
   int sect;
   int origHeight;

   States
   {
      RunScript:
         TNT1 A 1 Ceiling_LowerAndCrushDist(Num1, 150, 10000, 0, 1);
         
      Wait1:
         TNT1 A 1 A_JumpIf(!SectorUtil.IsMoving(sect), 1);
         Goto Wait1;
         
         TNT1 A 15 ZL_RadiusQuake(-1, 8, 15, 300, 200);
         TNT1 A 30 ZL_RadiusQuake(-1, 3, 15, 300, 200);   
         TNT1 A 0 Ceiling_RaiseByValue(Num1, 25, origHeight);
         
      Wait2:
         TNT1 A 1 A_JumpIf(!SectorUtil.IsMoving(sect), 1);
         Goto Wait2;
         
         TNT1 A 10;
         Goto EndScript;
   }
   
   override void OnRun()
   {
      sect = SectorUtil.GetFirstTagged(Num1);
      origHeight = SectorUtil.TotalHeight(sect);
   }
}


In action:



I do kind of perversely like this method of writing them, even if it's more code, and even if it means writing a bunch of useless TNT1 A's for every state. Can't put my finger on why I like it. TagWait equivalent functions are kinda clunky and also easy to hang the game with if you aren't paying attention. I haven't really used it in a real world situation yet though so I dunno how it would really hold up. Not sure the best way to handle, say, per-level variables, let alone ones that persist between levels. The SectorUtil stuff is part of a set of static functions I made to talk to level elements in a quicker and more convenient fashion as part of this. The ZL_* specials are just some glue I wrote to replicate how some ACS functions work that don't have exact counterparts in ZScript.

In conclusion: Sorry, please don't slap me.

Ok ok, melodramatic presentation aside, I just thought this was a fun experiment and wanted to share it in a more complete form instead of just briefly mentioning it from time to time.

Re: [ZScript] - Curious Rethink of Action Programming (C.R.A

Sun Sep 19, 2021 12:59 am

This is awesome! And such a massive improvement over Doom's regular crushers...

Re: [ZScript] - Curious Rethink of Action Programming (C.R.A

Sun Sep 19, 2021 1:13 am

This is cool! It feels more build-like. I wouldn't mind seeing more of this kind of thing.

Actually in DD1/SHDX/PA's codebase we use a system to cause chains of explosions as opposed to ACS. I've been looking to expand this system for other neat things.

I know ZScript has access to some things but I didn't think it could do this. I am intrigued by this. I'm confused to what the base class does though.

Re: [ZScript] - Curious Rethink of Action Programming (C.R.A

Sun Sep 19, 2021 1:28 am

SanyaWaffles wrote:This is cool! It feels more build-like. I wouldn't mind seeing more of this kind of thing.

Actually in DD1/SHDX/PA's codebase we use a system to cause chains of explosions as opposed to ACS. I've been looking to expand this system for other neat things.

I know ZScript has access to some things but I didn't think it could do this. I am intrigued by this. I'm confused to what the base class does though.


Base class for ZLSInstance? It's pretty much just a skeleton. The only reason it needs to exist is because I don't wanna have to put a crapload of different script actors to pick from when placing things in a level, rather you just place the main ZLevelScript actor and set a user var to pick what script you want to run - that being a string that just is the class name of whatever script class you made, inheriting from ZLSInstance.

Here's the whole shebang for the management classes:

Code:
class ZLevelScript : Actor
{
   string User_ScriptName;
   bool User_AutoRun;
   bool User_OnlyOnce;
   
   int User_Num1;
   int User_Num2;
   int User_Num3;
   int User_Num4;
   
   bool User_Flag1;
   bool User_Flag2;
   bool User_Flag3;
   bool User_Flag4;
   
   bool running;
   ZLSInstance curInstance;

   Default
   {
      //$Color 17
      //$Sprite internal:action
   
      Radius 8;
      +NOBLOCKMAP;
      +NOSECTOR;
      +NOGRAVITY;
      +NOINTERACTION;
      RenderStyle "None";
   }
   
   override void Activate(Actor a)
   {
      if (running)
      {
         return;
      }
      else
      {
         RunScriptInstance(User_ScriptName);
      }
   }
   
   override void Deactivate(Actor d)
   {
      if (running && curInstance != null)
      {
         curInstance.Abort();
      }
   }
   
   override void PostBeginPlay()
   {
      Super.PostBeginPlay();
      
      if (User_AutoRun)
      {
         RunScriptInstance(User_ScriptName);
      }
   }
   
   override void Tick()
   {
      Super.Tick();
      
      if (running)
      {
         // Script destroyed = script finished
         if (curInstance == null)
         {
            running = false;

            if (User_OnlyOnce)
               SetStateLabel("Null");
         }
      }
   }
   
   void RunScriptInstance(string scriptName)
   {
      class<Actor> instanceClass = scriptName;
      
      if (instanceClass)
      {
         curInstance = ZLSInstance(Spawn(instanceClass, Pos));
         
         curInstance.Num1 = User_Num1;
         curInstance.Num2 = User_Num2;
         curInstance.Num3 = User_Num3;
         curInstance.Num4 = User_Num4;
         
         curInstance.Flag1 = User_Flag1;
         curInstance.Flag2 = User_Flag2;
         curInstance.Flag3 = User_Flag3;
         curInstance.Flag4 = User_Flag4;
         
         curInstance.Run();
         running = true;
      }
      else
      {
         A_Log("Tried to run invalid script type " .. scriptName);
      }
   }
   
   static bool IsScriptRunning(int tid)
   {
      Actor a = ActorUtil.FetchSingleFromTID(tid);
      ZLevelScript sc = ZLevelScript(a);
      
      if (sc)
      {
         return sc.running;
      }
      else
      {
         return false;
      }
   }
}

class ZLSInstance : Actor
{
   int Num1;
   int Num2;
   int Num3;
   int Num4;
   
   bool Flag1;
   bool Flag2;
   bool Flag3;
   bool Flag4;

   Default
   {
      +NOBLOCKMAP;
      +NOGRAVITY;
      +NOINTERACTION;
      RenderStyle "None";
   }
   
   States
   {
      Spawn:
         TNT1 A 1;
         Loop;
         
      RunScript:
         TNT1 A 1;
         Goto EndScript;
         
      EndScript:
         TNT1 A 1;
         Stop;
   }
   
   void Run()
   {
      OnRun();
      SetStateLabel("RunScript");
   }
   
   virtual void OnRun() { }
   
   void Abort()
   {
      OnAbort();
      SetStateLabel("Null");
   }
   
   virtual void OnAbort() { }
}


I provide you with no warranty, I long ago learned to never fully trust my code 👍

edit:
Matt wrote:This is awesome! And such a massive improvement over Doom's regular crushers...


It just registered in my brain that you're Matt From The HDest, this is truly an honor, I have been too intimidated by that project to actually start playing it for well over a year now :D

If you're referring to the smoothness of the sector motion, that's currently done with some source code hax to how sector move functions operate - it's probably possible to hook into it with ZScript and manually drive doors and whatnot with it with custom acceleration profiles, but for my purposes (a standalone project incompatible with basically all other mods), it felt like it made more sense to just do the changes in source instead.

Re: [ZScript] - Curious Rethink of Action Programming (C.R.A

Sun Sep 19, 2021 4:07 am

nova++ wrote:I don't know if there is a scripting language holy war or anything going on

Spoiler:


This all seems really interesting and it's pushing new boundaries for what has been done so far with ZScript (which is very cool and innovative). I'm sure that something useful will come of it too and I understand your reluctance and difficulty (UDB on Linux I think you said has problems) to do stuff via ACS. However, personally, for the way I work, so far these ideas look to be more involved than typing a few lines of ACS and pressing "Save" to get the same end result.

I'm not saying don't go ahead, and clearly your enjoying doing it - which is value enough in itself - I just don't see any great advantage to my workflow (yet), but I'll happily use something if/when I do. :D

Re: [ZScript] - Curious Rethink of Action Programming (C.R.A

Sun Sep 19, 2021 10:04 am

Enjay wrote:This all seems really interesting and it's pushing new boundaries for what has been done so far with ZScript (which is very cool and innovative). I'm sure that something useful will come of it too and I understand your reluctance and difficulty (UDB on Linux I think you said has problems) to do stuff via ACS. However, personally, for the way I work, so far these ideas look to be more involved than typing a few lines of ACS and pressing "Save" to get the same end result.

I'm not saying don't go ahead, and clearly your enjoying doing it - which is value enough in itself - I just don't see any great advantage to my workflow (yet), but I'll happily use something if/when I do. :D



Yeah, understandable. It definitely is a bit more involved, there. The workflow I see is making a .zs file with all the scripts for a given map or cluster, and keeping it open in a text editor on the side, and when you need a new script just copy-pasting a template with the boilerplate code to get one working. I like the potential extra stuff you can do with this over ACS, but I have not yet used it in a real world scenario, so it's difficult to say how much that potential translates into reality.

In parallel I also made a python script to automatically run all ACS source code in a folder through ACC, put the binaries in the proper folder, and add them all to a LOADACS entry, which does ease the struggles I had with built in text editors, so I guess I'm keeping my options open.

It would be nice if there was a cleaner way to write some of this syntax, such as not having to do TNT1 A all the time, and just being able to put, I dunno, "-" instead to signify a dummy sprite.


I admit I am unsure how far I should go with this in a practical sense, I feel like to some degree I am just being contrarian, and should just actually use ACS for real level work when I get to that point in my project. I dunno. I just feel substantially less confident in the idea today for some reason.

Re: [ZScript] - Curious Rethink of Action Programming (C.R.A

Sun Sep 19, 2021 5:36 pm

Well, I figured out one way to get level-wide variables working at least. I stumbled onto this: https://zdoom.org/wiki/ZScript_global_variables

So from that I set up a Thinker that auto-loads and is referenced by the ZLevelScript objects, which then pass it on to their child ZLSInstance objects when spawned. Inside this thinker is a Dictionary that keeps all the variables stored as strings. Still working out how to deal with dictionaries in ZScript properly, though. They are a bit barebones compared to the C# ones I'm used to working with. Here's what that looks like though:

Code:
class ZLSVars : Thinker
{
   Dictionary intDict;
   
   ZLSVars Init()
   {
      intDict = Dictionary.Create();
      ChangeStatNum(STAT_INFO);
      return self;
   }
   
   static ZLSVars Get()
   {
      let itr = ThinkerIterator.Create("ZLSVars", STAT_INFO);
      let v = ZLSVars(itr.Next());
      
      if (v == null)
      {
         v = new("ZLSVars").Init();
      }
      
      return v;
   }
   
   void WriteInt(string name, int value)
   {
      intDict.Insert(name, String.Format("%d", value));
   }
   
   int ReadInt(string name)
   {
      string found = intDict.At(name);   
      return found.ToInt(10);
   }
}


And what do you know, it works:

Image

Across saves too, yay.

The syntax for accessing it is a bit cumbersome, though, that's the main downside. They're stored as strings since dictionaries only support strings, so I can't modify them directly. See here:

Code:
void CountActivations()
{
   int c = vars.ReadInt("WTest_CrushCount");
   vars.WriteInt("WTest_CrushCount", c + 1);
   A_Log("Crusher has been activated " .. (c + 1) .. " times!");
}


Wouldn't hurt to add an IncInt and DecInt function to it too, I guess.

I can't figure out how to check whether a dictionary contains a key, though.