Ijon 4 years ago
commit
7986a015ab

+ 3 - 0
.gitignore

@@ -0,0 +1,3 @@
+*.pk3
+*.zip
+nocommit/*

+ 1 - 0
CHANGELOG.txt

@@ -0,0 +1 @@
+

+ 1 - 0
README.txt

@@ -0,0 +1 @@
+

+ 30 - 0
acsprofile.txt

@@ -0,0 +1,30 @@
+Module       script                    Total    Runs     Avg     Min     Max
+------------ -------------------- ---------- ------- ------- ------- -------
+ARC_CLEA     192                        1970       1    1970    1970    1970
+ARC_CLEA     193                          97       1      97      97      97
+
+Top 2000 functions:
+Module       function                  Total    Runs     Avg     Min     Max
+------------ -------------------- ---------- ------- ------- ------- -------
+ARC_CLEA     look_loop                  1204       1    1204    1204    1204
+ARC_CLEA     target_setslot              796       2     398     391     405
+ARC_CLEA     target_add                  348       2     174     174     174
+ARC_CLEA     defaulttid                  270       5      54      27      72
+ARC_CLEA     _defaulttid                 250       5      50      23      68
+ARC_CLEA     tgt_applyslots              236       2     118     111     125
+ARC_CLEA     tgt_setdatabytid            224       4      56      56      56
+ARC_CLEA     target_clearslot            209       1     209     209     209
+ARC_CLEA     tgt_flipfriend              206       4      51      51      52
+ARC_CLEA     tgt_add                     198       2      99      99      99
+ARC_CLEA     tgt_find                    194      10      19      14      23
+ARC_CLEA     tgt_del                     176       2      88      88      88
+ARC_CLEA     tgt_prune                   152       1     152     152     152
+ARC_CLEA     target_clear                112       1     112     112     112
+ARC_CLEA     tgt_clear                   110       1     110     110     110
+ARC_CLEA     unusedtid                    84       4      21      21      21
+ARC_CLEA     loop_calcscore               74       1      74      74      74
+ARC_CLEA     ftoi                         48      12       4       4       4
+ARC_CLEA     distance                     48       1      48      48      48
+ARC_CLEA     magnitudethree_f             37       1      37      37      37
+ARC_CLEA     min                          36       6       6       6       6
+ARC_CLEA     tgt_setdata                  29       1      29      29      29

+ 30 - 0
pk3/DECORATE.dec

@@ -0,0 +1,30 @@
+#include "decorate/core.dec"
+#include "decorate/projectile.dec"
+
+actor ArcFriendly: CustomInventory
+{
+    States
+    {
+      Spawn:
+        TNT1 A 0
+        stop
+
+      Pickup:
+        TNT1 A 0 A_ChangeFlag("FRIENDLY", 1)
+        stop
+    }
+}
+
+actor ArcUnfriendly: CustomInventory
+{
+    States
+    {
+      Spawn:
+        TNT1 A 0
+        stop
+
+      Pickup:
+        TNT1 A 0 A_ChangeFlag("FRIENDLY", 0)
+        stop
+    }
+}

+ 13 - 0
pk3/LICENSE.txt

@@ -0,0 +1,13 @@
+           DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 
+                   Version 2, December 2004 
+
+Copyright (C) 2016 Ijon Tichy <jino.itchy@gmail.com>
+
+Everyone is permitted to copy and distribute verbatim or modified 
+copies of this license document, and changing it is allowed as long 
+as the name is changed. 
+
+           DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 
+  TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 
+
+ 0. You just DO WHAT THE FUCK YOU WANT TO.

+ 1 - 0
pk3/LOADACS.txt

@@ -0,0 +1 @@
+arc_clean

+ 95 - 0
pk3/acs/arc_angle.h

@@ -0,0 +1,95 @@
+// We need two pairs of angles, one for the current arcer so it faces the new
+//  arcer, and one for the new arcer so we know what direction it should face.
+//
+// The arcer's new angle will be somewhere between the direction the current
+//  arcer is facing, and the direction the new target is in. Where exactly is
+//  controlled by ADATA_INT_FACINGWEIGHT.
+//
+// A weight of 0 means "face the same direction as the current arcer". In effect,
+//  go straight forward.
+//
+// A weight of 1 means "face the target". In effect, bounce around like a fool.
+//
+// Note that you aren't limited to values between 0 and 1. If for some reason
+//  you want the new arc to *overshoot* the direction to face the target, you
+//  can - use a value greater than 1. If you want it to face further away from
+//  the target (effectively curving inward), use a value less than 0.
+// 
+
+#define ANG_TARGET  0
+#define ANG_NEW     1
+
+#define ANG_ANGLE   0
+#define ANG_PITCH   1
+
+int Arc_NewAngles[2][2];
+
+function void Arc_CalcNewAngle(int arcType, int arcerTID, int targetTID)
+{
+    int faceWeight = INT_ArcData[arcType][ADATA_INT_FACINGWEIGHT];
+    int faceFirer  = INT_ArcData[arcType][ADATA_INT_FACINGFIRER];
+
+    // Stealing the firer's direction instead of the current arcer's? Sure.
+    if (faceFirer)
+    {
+        SetActivator(arcerTID, AAPTR_TARGET);
+    }
+
+    int arcerAngle = GetActorAngle(0);
+    int arcerPitch = GetActorPitch(0);
+    
+    SetActivator(arcerTID);
+
+    int dX = GetActorX(targetTID) - GetActorX(arcerTID);
+    int dY = GetActorY(targetTID) - GetActorY(arcerTID);
+    int dZ = GetActorZ(targetTID) - GetActorZ(arcerTID);
+
+    // Remember trigonometry class!
+    //
+    // The angle of a 2D vector is the arc tangent defined by its X and Y values.
+    //
+    //      /|
+    //     / |           Y/X = tan o
+    //  Z /  | Y    atan Y/X =     o
+    //   /   |
+    //  /    |   And that's what VectorAngle is basically; the atan function.
+    // o_____'   Bam, angles.
+    //    X
+    //
+    // Also, a pitch looking up is negative, hence -dZ.
+
+    int targetAngle = VectorAngle(dX, dY);
+    int targetPitch = VectorAngle(magnitudeTwo_f(dX, dY), -dZ);
+    
+    // We aren't necessarily taking the shortest path from one angle to another
+    //  right now. If we're going from arcer angle 0 to target angle 0.9, the
+    //  below line suggests 0.9, not -0.1.
+    //
+    // Similarly, going from arcer angle 0.6 to target angle 0, the below line
+    //  suggests -0.6 and not 0.4.
+
+    int angleDiff = targetAngle - arcerAngle;
+
+    // This corrects that.
+    if (angleDiff < -0.5) { angleDiff += 1.0; }
+    if (angleDiff >  0.5) { angleDiff -= 1.0; }
+
+
+    // Pitches can only be between -0.25 and 0.25 anyway, so no big deal here.
+    int pitchDiff = targetPitch - arcerPitch;
+
+
+    // Get the in between...
+    int middleAngle = arcerAngle + FixedMul(faceWeight, angleDiff);
+    int middlePitch = arcerPitch + FixedMul(faceWeight, pitchDiff);
+
+
+    // And assign shit!
+    
+    Arc_NewAngles[ANG_TARGET][ANG_ANGLE] = targetAngle;
+    Arc_NewAngles[ANG_TARGET][ANG_PITCH] = targetPitch;
+    
+    Arc_NewAngles[ANG_NEW][ANG_ANGLE] = middleAngle;
+    Arc_NewAngles[ANG_NEW][ANG_PITCH] = middlePitch;
+}
+

+ 65 - 0
pk3/acs/arc_clean.c

@@ -0,0 +1,65 @@
+#include "zcommon.acs"
+#library "arc_clean"
+
+// I've forgotten half the crap in here. The function names hopefully explain enough.
+#include "commonFuncs.h"
+
+// Concepts you should be aware of:
+//
+// Targeting data:
+//  Without special handling, A_LookEx will always find the same target, and it'll
+//   be the first thing that a counterclockwise fan starting at due east hits,
+//   damn how far away it is (as long as it's within range at all). So this ACS
+//   has a system dedicated towards flipping the +FRIENDLY/-FRIENDLY flag on
+//   any targets found, so that A_LookEx will actually look past them.
+//
+//  The first iteration of this arc code handled them... haphazardly. It took a
+//   while to actually get it so that all the flags would be flipped properly,
+//   and oftentimes one or two targets would slip by, causing, say, demons to
+//   turn against their allies the moment you hit them with an arc.
+//
+//  The second iteration handled it slowly, which
+//
+//  I intend to avoid all of that this time.
+
+
+// arc_const.h:
+//   Stores constant definitions that you're meant to mess with.
+//   Script numbers, arc definitions, stuff like that.
+//   You can modify anything in this file, but NOT delete it.
+//
+// arc_user_const.h:
+//   It's like arc_const.h, but meant for stuff in arc_user.h.
+//   You can delete everything in here and the arc code will still work.
+//   Just make sure to delete everything in arc_user.h as well.
+//
+// arc_target.h:
+//   This defines the core functions for managing targeting data. Don't use
+//   these directly.
+//
+// (arc_target_core.h and arc_target_user.h were the old system, the slow one)
+//
+// arc_look.h:
+//   The nitty-gritty of how the arc acquires targets. It jumps between ACS and
+//   DECORATE to get the job done, a messy but unavoidable fact.
+//
+// arc_angle.h:
+//   This entire file is dedicated to calculating the angle of new arcers, as
+//   well as the angle between an arcer and its target.
+//
+// arc_main.h:
+//   Where most of it happens. This is where the main arc script is, the one
+//   that handles shuffling pointers around, keeping the arc chain going, and
+//   all that stuff.
+//
+// arc_user.h:
+//   None of this is necessary for the arc stuff to run. Stick your auxillary
+//   scripts and functions and whatnot in here.
+
+#include "arc_const.h"
+#include "arc_user_const.h"
+#include "arc_target.h"
+#include "arc_look.h"
+#include "arc_angle.h"
+#include "arc_main.h"
+#include "arc_user.h"

BIN
pk3/acs/arc_clean.o


+ 66 - 0
pk3/acs/arc_const.h

@@ -0,0 +1,66 @@
+// All constant definitions that can be changed freely are here.
+//  Do not *delete* anything, though. All the definitions and arrays are
+//  necessary, and deleting them will cause compiling to inevitably fail.
+//
+// Well, okay, you probably don't need to mess with the ARCDATA_* and ADATA_*
+//  stuff. But the STR_ArcData and INT_ArcData stuff is fine.
+
+// Core arc script number. This is also in decorate/core.dec, so be sure
+//  to change it there as well.
+
+#define ARC_MAIN        191
+#define ARC_CLEAR       192
+
+
+
+// Arc types need to be defined in ACS, because there's no wayt to store all
+//  the information on DECORATE actors (namely strings), and plus that'd be
+//  ripe for breakage.
+
+#define ARCTYPES    1
+
+// Arcer properties. Due to ACS limitations, ints and strings have to be stored
+//  in different arrays.
+
+#define ARCDATA_INT 6
+#define ARCDATA_STR 1
+
+// Maximum distance arc can look.
+#define ADATA_INT_MAXRANGE      0
+
+// How much should the angle between the target and arcer matter?
+#define ADATA_INT_ANGLEWEIGHT   1
+
+// Which direction should the new arcer face? Towards the new target (1.0),
+//  in the same direction the current arc is facing (0), or in between?
+#define ADATA_INT_FACINGWEIGHT  2
+
+// Instead of the current arc, should we use the *firer's* angle?
+//  A_BFGSpray antics ahoy!
+#define ADATA_INT_FACINGFIRER   3
+
+// Should this arcer not automatically clear target data when it's done arcing?
+//      
+// This is meant for spawning multiple arcers at once that shouldn't overlap.
+//  Make damn sure you call ARC_CLEAR when you're done.
+#define ADATA_INT_MANUALCLEAR   4
+
+
+// Do we *really* need to be in the center of the actor? This adds random variance
+//  to where the new arc actor spawns. It's not based off the target's height
+//  and width because Zandronum doesn't have that, but once it gets it,
+//  it probably will be.
+#define ADATA_INT_RANDOMOFFSET  5
+
+
+// Name of arc actor to be spawned on a found target.
+#define ADATA_STR_NEXTARCER 0
+str STR_ArcData[ARCTYPES][ARCDATA_STR] =
+{
+    {"ArcTest"},
+};
+
+int INT_ArcData[ARCTYPES][ARCDATA_INT] = 
+{
+    {256.0, 1.0,   0.5, false, false, 8.0},
+};

+ 248 - 0
pk3/acs/arc_look.h

@@ -0,0 +1,248 @@
+// Simply looking for shit needed an entire file dedicated to it.
+
+// Copying here for self-reminder:
+//
+//      --- LOOP ---
+//
+//      Step 5 (A): Send arcer to Arc_Look
+//       Pointers: {firer, ????, ????}
+//
+//      Step 6 (D): Look around
+//       Pointers: {firer, ????, potential target}
+//
+//      Step 7 (A): Nothing in tracer pointer? Break loop
+//       Pointers: {firer, ????, potential target}
+//
+//      Step 8 (A): Something in tracer pointer? Process it, add T_CHECKED target slot to it
+//       Pointers: {firer, ????, potential target}
+//
+//      Step 9 (A): New target processed, decide if it's better than current best, if so keep it
+//       Pointers: {firer, ????, potential target}
+//
+//      --- END LOOP ---
+//
+// Step 10 (A): Clear all T_CHECKED target slots
+//  Pointers: {firer, ????, now-irrelevant target}
+
+int APos_X, APos_Y, APos_Z;
+int ALook_AngX, ALook_AngY, ALook_AngZ;
+
+int IsZand;
+
+function int Look_Loop(int arcType, int arcerTID)
+{
+    int bestTargetTID = 0;
+    int bestTargetScore = 0x7FFFFFFF;
+
+    int rangeStep = 0;
+    int rangeMax  = INT_ArcData[arcType][ADATA_INT_MAXRANGE];
+
+    SetDBEntry("arc", "iszand", 1);
+    IsZand = GetDBEntry("arc", "iszand") == 1;
+
+    // Precalculate to save cycles. It's used in Loop_CalcScore.
+    // 
+    // It's the vector of the angle the arc is facing.
+    
+    APos_X = GetActorX(0);
+    APos_Y = GetActorY(0);
+    APos_Z = GetActorZ(0);
+    int arcAngle = GetActorAngle(0);
+    int arcPitch = GetActorPitch(0);
+
+    ALook_AngX = FixedMul(cos(arcAngle), cos(arcPitch));
+    ALook_AngY = FixedMul(sin(arcAngle), cos(arcPitch));
+    ALook_AngZ =          sin(arcPitch);
+
+    while (true)
+    {
+        rangeStep += 32.0;
+        SetUserVariable(0, "user_lookDist", ftoi(min(rangeStep, rangeMax)));
+
+        while (true)
+        {
+            // Steps 5 and 6
+            SetActorState(0, "Arc_Look");
+
+            // Step 7
+            if (CheckInventory("ArcItem_NoTarget")) { break; }
+
+            // Step 8
+            SetActivator(0, AAPTR_TRACER);
+
+            int targetTID   = defaultTID(-1);
+            int targetScore = Loop_CalcScore(arcType, arcerTID, targetTID, rangeMax, bestTargetScore);
+
+            TCheck_Add(targetTID);
+
+            // Step 9
+            if ((targetScore != -1) && (targetScore < bestTargetScore))
+            {
+                bestTargetTID   = targetTID;
+                bestTargetScore = targetScore;
+
+                rangeMax  = targetScore;
+                rangeStep = min(rangeMax, rangeStep);
+
+                // Saves a few cycles to just break it now.
+                //  And goddamn, every instruction saved is important.
+                if (rangeMax == rangeStep) { break; }
+
+                SetUserVariable(0, "user_lookDist", ftoi(rangeStep));
+            }
+        }
+
+        if (rangeStep >= rangeMax)       { break; }
+        if (bestTargetScore < rangeStep) { break; }
+    }
+
+    // Step 10
+    TCheck_Clear();
+
+    SetActivator(arcerTID);
+    return bestTargetTID;
+}
+
+
+
+
+// Given the TIDs of both the arcer and its potential target, as well as
+//  the arc type, calculate its targeting score. Lower is better. If the
+//  target's invalid, return -1 instead.
+//
+// We pass in currentBestScore for some score-calculating optimizations.
+//
+// Thing is with the score calculation is that it starts at the actor's distance
+//  from the arcer, and only goes up from there. So if the distance is higher than
+//  the current best score, there is no way it will beat the current best, so we
+//  can just return it as-is. Every calculation afterwards is meaningless.
+
+function int Loop_CalcScore(int arcType, int arcerTID, int targetTID, int maxRange, int currentBestScore)
+{
+    // First off, we check off the quick-to-calculate stuff.
+    
+    // Gather firer and target info.
+    SetActivator(0, AAPTR_TARGET);
+    int firerPln  = PlayerNumber();
+    int firerTID  = ActivatorTID();
+
+    SetActivator(targetTID);
+    int targetPln = PlayerNumber();
+
+    SetActivator(arcerTID);
+
+    // Have we somehow targeted ourself? 
+    if (firerTID == targetTID)
+    {
+        Log(s:"Somehow we targeted ourself: ", d:firerTID, s:" vs. ", d:targetTID);
+        return -1;
+    }
+
+
+    // If the target and firer are players, we check player numbers and teams,
+    //  because friendliness checks aren't a reliable way of doing it.
+    if ((targetPln != -1) && (firerPln != -1))
+    {
+        int noTeamIndex;
+
+        // ZDoom has the "no team" index at 255. Zandronum has it at 4. DUMB.
+        if (IsZand) { noTeamIndex = 4; }
+        else        { noTeamIndex = 255; }
+
+        // A -FRIENDLY arc will target the player who fired it. Rule that out.
+        if (firerPln == targetPln) { return -1; }
+
+        int firerTeam  = GetPlayerInfo(firerPln,  PLAYERINFO_TEAM);
+        int targetTeam = GetPlayerInfo(targetPln, PLAYERINFO_TEAM);
+
+        // And let's make sure we're not team-killing.
+        if ((firerTeam != noTeamIndex) && (firerTeam == targetTeam)) { return -1; }
+    }
+    else
+    {
+        // Friendliness check - same friendliness flags? Not valid.
+        if (GetActorProperty(firerTID,  APROP_Friendly) == GetActorProperty(targetTID, APROP_Friendly))
+        {
+            return -1;
+        }
+    }
+
+    // At this point, we're pretty damn sure we have a valid target.
+    //  Now only distance checks will rule them out.
+
+    // Any score above this can be ruled out.
+    int rangeLimiter = min(maxRange, currentBestScore);
+
+    int targetX = GetActorX(targetTID);
+    int targetY = GetActorY(targetTID);
+    int targetZ = GetActorZ(targetTID) + GetActorViewHeight(targetTID);
+
+    int dX = targetX - APos_X;
+    int dY = targetY - APos_Y;
+    int dZ = targetZ - APos_Z;
+
+    // dX and dY were already implicitly checked by A_LookEx, but not dZ,
+    //  so let's handle that to quickly rule out targets too high or low up.
+    if (abs(dZ) >= rangeLimiter) { return -1; }
+
+
+    // Well, that's all the fast checks out of the way. Time for slow stuff.
+
+    int distanceScore = magnitudeThree_f(dX, dY, dZ);
+
+    // If the distance is higher than the current best score, there's no way
+    //  it can beat it when adjusted, so disqualify it. Also, range limits.
+    if (distanceScore >= rangeLimiter) { return -1; }
+
+
+    // Time to factor in angle difference to the target's score.
+    //
+    // First off, we're just gonna grab this now. If it's 0,
+    //  it saves us a lot of time, because angle won't matter.
+
+    int angleWeight = INT_ArcData[arctype][ADATA_INT_ANGLEWEIGHT];
+    if (angleWeight <= 0) { return distanceScore; }
+
+
+    // Okay, now we need to weigh the angle.
+    // 
+    // Calculate difference between 3D angles.
+    //
+    //      acos(dot3(vec1, vec2) / (mag(vec1) * mag(vec2)))
+    //
+    // Since the arc angle vector's (vec1 here) magnitude always 1,
+    //  this just becomes:
+    //
+    //      acos(dot3(vec1, vec2) / mag(vec2))
+    //
+    //  angleDiff will never be above 0.5.
+
+    int angleDiff = acos(FixedDiv(dot3(ALook_AngX, ALook_AngY, ALook_AngZ, dX, dY, dZ), distanceScore));
+
+
+    // Now calculate the score. We can only go up from distanceScore, or else
+    //  some core assumptions for (very necessary) optimization are broken.
+    //
+    // Basically, this: if the best score is 128, anything at least 128 units
+    //  away cannot, under any circumstances, beat that score. This means we
+    //  can stop looking past 128 units because it's a complete waste of time,
+    //  which allows us to look in layers and stop exactly when it becomes
+    //  pointless.
+    //
+    // As for why we do FixedDiv, it's because of how as 1/x approaches x=0,
+    //  the rate of change gets faster and faster. For us, that means angles
+    //  get weighted much more harshly the further away they stray from the
+    //  zero-line.
+
+    int angleScore = FixedDiv(distanceScore, 1.0 - angleDiff);
+
+    // Angle weight scale goes from 0 to 1.
+    if (angleWeight >= 1.0)
+    {
+        return angleScore;
+    }
+
+    
+    int weightedScore = distanceScore + FixedMul(angleWeight, angleScore - distanceScore);
+    return weightedScore;
+}

+ 237 - 0
pk3/acs/arc_main.h

@@ -0,0 +1,237 @@
+// ========================= REALLY IMPORTANT GOD DAMN =========================
+//
+// (this is in decorate/core.dec as well because it's THAT IMPORTANT)
+//
+// You REALLY REALLY should understand the process that arcing takes, so here it is,
+//  in very brief detail.
+// 
+// Pointers are in order {target, master, tracer}. "????" means "unset so far".
+//
+// "(D)" means we're running in DECORATE. "(A)" means we're running in ACS.
+//
+// Step 1  (D/A): call arcing script
+//  Pointers: {firer, ????, ????}
+//
+// Step 2  (A): ACS sends arcer to Arc_Query
+//  Pointers: {firer, ????, ????}
+//
+// Step 3  (D): Arc_Query's run, gives ArcItem_DoArc to itself if it wants to arc
+//  Pointers: {firer, ????, ????}
+//
+// Step 4  (A): If no ArcItem_DoArc, clear target data, send arcer to Arc_EndArc, and end
+//  Pointers: {firer, ????, ????}
+//
+// - IF NOT ENDED -
+//
+//      --- LOOP ---
+//
+//      Step 5 (A): Send arcer to Arc_Look
+//       Pointers: {firer, ????, ????}
+//
+//      Step 6 (D): Look around
+//       Pointers: {firer, ????, potential target}
+//
+//      Step 7 (A): Nothing in tracer pointer? Break loop
+//       Pointers: {firer, ????, potential target}
+//
+//      Step 8 (A): Something in tracer pointer? Process it, add T_CHECKED target slot to it
+//       Pointers: {firer, ????, potential target}
+//
+//      Step 9 (A): New target processed, decide if it's better than current best, if so keep it
+//       Pointers: {firer, ????, potential target}
+//
+//      --- END LOOP ---
+//
+// Step 10 (A): Clear all T_CHECKED target slots
+//  Pointers: {firer, ????, now-irrelevant target}
+//
+// Step 11 (A): No target? Clear target data, Stick last target (if any) in tracer field,
+//              send arcer to Arc_NoTarget, end
+//  Pointers: {firer, ????, last target (if any)}
+//
+// - IF NOT ENDED -
+//
+// Step 12 (A): Stick best target in tracer field
+//  Pointers: {firer, ????, best target}
+//
+// Step 13 (D): Do Arc_FoundTarget (now's the time to do damage)
+//  Pointers: {firer, ????, best target}
+//  
+// Step 14 (A): Stick T_HIT target slot on calculated best target (it is now +FRIENDLY)
+//  Pointers: {firer, ????, best target}
+//
+// Step 15 (A): Spawn next arcer object on best target, focus on that one now
+//  Pointers: {empty, empty, empty}
+//
+// Step 16 (A): Set up pointers on new arcer object
+//  Pointers: {firer, current arcer, current target}
+//
+// Step 17 (A): Send new arcer object to Arc_Spawn
+
+script ARC_MAIN (int arcType)
+{
+    int manualClear = INT_ArcData[arcType][ADATA_INT_MANUALCLEAR];
+
+    // Step 1 already done
+    int arcerTID   = defaultTID(-1);
+    SetActivatorToTarget(arcerTID);
+
+    int firerTID = defaultTID(-1);
+
+    SetActivator(arcerTID, AAPTR_TRACER);
+    int oldTargetTID;
+    
+    if (ClassifyActor(0) & ACTOR_WORLD) { oldTargetTID = 0; }
+    else                                { oldTargetTID = defaultTID(-1); }
+
+    SetActivator(arcerTID);
+
+    // Steps 2 and 3
+    TakeInventory("ArcItem_DoArc", 0x7FFFFFFF);
+    SetActorState(0, "Arc_Query");
+    int doArc = CheckInventory("ArcItem_DoArc");
+
+    // Step 4
+    if (!doArc)
+    {
+        if (!manualClear) { T_Clear(); }
+        SetActorState(0, "Arc_EndArc");
+        terminate;
+    }
+
+    // Steps 5 through 10
+    int targetTID = Look_Loop(arcType, arcerTID);
+
+    // Step 11
+    if (targetTID == 0)
+    {
+        if (!manualClear) { T_Clear(); }
+
+        SetPointer(AAPTR_TRACER, oldTargetTID);
+        SetActorState(0, "Arc_NoTarget");
+        terminate;
+    }
+
+    // Step 12
+    SetPointer(AAPTR_TRACER, targetTID);
+
+    // We actually need to get these *before* FoundTarget runs, because
+    //  if the target dies, his camera height goes to 7.
+    int spawnX = GetActorX(targetTID);
+    int spawnY = GetActorY(targetTID);
+    int spawnZ = GetActorZ(targetTID) + GetActorViewHeight(targetTID);
+
+    // Being in the exact center of the target gets boring. Let's vary it up.
+
+    int randomOffset = INT_ArcData[arctype][ADATA_INT_RANDOMOFFSET];
+
+    spawnX += random(-randomOffset, randomOffset);
+    spawnY += random(-randomOffset, randomOffset);
+    spawnZ += random(-randomOffset, randomOffset);
+
+    // Step 13, with an addition:
+    //  - We also set the arcer's angle now that we have a target. It's in
+    //    between the firer's angle and the angle pointing towards the new
+    //    target.
+
+    Arc_CalcNewAngle(arcType, arcerTID, targetTID);
+    SetActorAngle(arcerTID, Arc_NewAngles[ANG_TARGET][ANG_ANGLE]);
+    SetActorPitch(arcerTID, Arc_NewAngles[ANG_TARGET][ANG_PITCH]);
+
+    SetActorState(0, "Arc_FoundTarget");
+
+    // Step 14
+
+    // Looking at the code alone, you don't know how much anguish went into the
+    //  four lines below. And it's just four lines. How bad could it be?
+    //
+    // It was either these four lines or five billion ugly hacks to get around
+    //  a useless fucking ID system that does absolutely nothing an ID system
+    //  is meant to actually do.
+    //
+    // So, Thing IDs. Let's run down the list of what IDs tend to do:
+    //
+    // 1. They identify something.
+    //
+    // 2. They *uniquely* identify something.
+    //
+    // 3. They uniquely identify something *immutably*.
+    //
+    // Lets look at Thing IDs.
+    //
+    // 1. They may identify something. They may identify a lot of things. Who
+    //    knows? You sure as hell don't. ThingCount won't actually tell you.
+    //    Dead things don't count. Sometimes.
+    //
+    // 2. And yeah, they can identify multiple things. Which would be fine,
+    //    if it were a group ID. But half the functions are clearly intended
+    //    to work with singular entities, and what's the only thing they accept?
+    //    Thing IDs.
+    //
+    // 3. What they identify can change. In fact, it's safer to say what they
+    //    identify *will* change. Why? Because anything can change the Thing ID
+    //    of something at any time. Anything can be given a given Thing ID at
+    //    any time. There's absolutely no guarantees for anything.
+    //
+    // "But there's gotta be a better system!"
+    //
+    // Nope. They all got shot down. Some of them were even *coded* for the
+    //  ZDoom developers. Fuck that. Graf shot them down anyway.
+    //
+    // So you're stuck with this ungodly abomination of an ID system that 
+    //  breaks if you so much as breath on it.
+    //
+    // These four lines below take away the TID from the arcer target if it
+    //  died. Without them... well, take them out, go fire some arcs into a
+    //  crowd for a while, and you'll find out.
+    //
+    // The moment a target gets their TID assigned to the same TID a dead
+    //  monster has... it's over.
+    //
+    // TIDs are a joke played on every ZDoom modder that's ever had the
+    //  misfortune of using them.
+
+    if (GetActorProperty(targetTID, APROP_Health) <= 0)
+    {
+        Thing_ChangeTID(targetTID, 0);
+        targetTID = 0;
+    }
+    else
+    {
+        THit_Add(targetTID);
+    }
+
+    // Step 15
+    int newTID   = unusedTID(6000, 9000);
+    int newActor = STR_ArcData[arcType][ADATA_STR_NEXTARCER];
+
+    // See Arc_CalcNewAngles for details as to how the new arcer's angle
+    //  is determined.
+
+    SpawnForced(newActor, spawnX, spawnY, spawnZ, newTID);
+    SetActorAngle(newTID, Arc_NewAngles[ANG_NEW][ANG_ANGLE]);
+    SetActorPitch(newTID, Arc_NewAngles[ANG_NEW][ANG_PITCH]);
+
+    // Step 16
+    SetActivator(newTID);
+
+    SetPointer(AAPTR_TARGET, firerTID);
+    SetPointer(AAPTR_MASTER, arcerTID);
+    SetPointer(AAPTR_TRACER, targetTID);
+
+    // Step 17
+    SetActorState(0, "Arc_Spawn");
+    
+    // This will run after everything in the new arcer is done,
+    //  so it's safe to add this here.
+    if (!manualClear) { T_Clear(); }
+}
+
+
+
+// This is for when the arc object doesn't automatically do Target_Clear().
+
+script ARC_CLEAR (void)
+{
+    T_Clear();
+}

+ 144 - 0
pk3/acs/arc_target.h

@@ -0,0 +1,144 @@
+#define T_ROWS 65536
+
+world int 43:T_FriendState[];
+world int 44:THit_TIDToRow[];
+
+int THit[T_ROWS];
+
+int THitCount = 0;
+
+world int 45:TCheck_TIDToRow[];
+
+int TCheck[T_ROWS];
+
+int TCheckCount = 0;
+
+
+function void T_SetFriend(int tid, int state)
+{
+    int makeFriend  = (!!state) ^ T_FriendState[tid];
+    int friendState = GetActorProperty(tid, APROP_Friendly); 
+
+    if (friendState == makeFriend) { return; }
+
+    if (makeFriend) { GiveActorInventory(tid, "ArcFriendly",   1); }
+    else            { GiveActorInventory(tid, "ArcUnfriendly", 1); }
+}
+
+function int THit_FindTID(int tid)
+{
+    int ret = THit_TIDToRow[tid] - 1;
+    if (ret >= THitCount) { return -1; }
+
+    if (THit[ret] != tid) { return -1; }
+    return ret;
+}
+
+function int TCheck_FindTID(int tid)
+{
+    int ret = TCheck_TIDToRow[tid] - 1;
+    if (ret >= TCheckCount) { return -1; }
+    return ret;
+}
+
+function int THit_Add(int tid)
+{
+    int row = THitCount++;
+
+    THit[row]           = tid;
+    THit_TIDToRow[tid]  = row + 1;
+
+    int otherRow = TCheck_FindTID(tid);
+
+    if (otherRow == -1)
+    {
+        T_FriendState[tid] = GetActorProperty(tid, APROP_Friendly);
+        T_SetFriend(tid, true);
+
+        if (T_FriendState[tid])
+        {
+            GiveActorInventory(tid, "ArcThingCountTest", 1);
+        }
+    }
+
+    return row;
+}
+
+function int TCheck_Add(int tid)
+{
+    int row = TCheckCount++;
+
+    TCheck[row]             = tid;
+    TCheck_TIDToRow[tid]    = row + 1;
+
+    int otherRow = THit_FindTID(tid);
+
+    if (otherRow == -1)
+    {
+        T_FriendState[tid] = GetActorProperty(tid, APROP_Friendly);
+        T_SetFriend(tid, true);
+
+        if (T_FriendState[tid])
+        {
+            GiveActorInventory(tid, "ArcThingCountTest", 1);
+        }
+    }
+    
+    return row;
+}
+
+function void THit_Clear(void)
+{
+    int i;
+
+    for (i = 0; i < THitCount; i++)
+    {
+        int tid = THit[i];
+        int otherRow = TCheck_FindTID(tid);
+
+        if (otherRow == -1)
+        {
+            T_SetFriend(tid, false);
+
+            if (GetActorProperty(tid, APROP_Health) <= 0)
+            {
+                Thing_ChangeTID(tid, 0);
+            }
+        }
+
+        THit_TIDToRow[tid] = 0;
+    }
+    
+    THitCount = 0;
+}
+
+function void TCheck_Clear(void)
+{
+    int i;
+
+    for (i = 0; i < TCheckCount; i++)
+    {
+        int tid = TCheck[i];
+        int otherRow = THit_FindTID(tid);
+
+        if (otherRow == -1)
+        {
+            T_SetFriend(tid, false);
+
+            if (GetActorProperty(tid, APROP_Health) <= 0)
+            {
+                Thing_ChangeTID(tid, 0);
+            }
+        }
+
+        TCheck_TIDToRow[tid] = 0;
+    }
+
+    TCheckCount = 0;
+}
+
+function void T_Clear(void)
+{
+    THit_Clear();
+    TCheck_Clear();
+}

+ 675 - 0
pk3/acs/arc_target_core.h

@@ -0,0 +1,675 @@
+/*
+ * ===========================================================================
+ * ======================= IMPORTANT THING LOOKEE HERE =======================
+ * 
+ * This is where core functions required for arc_target_user.h are located.
+ *  You probably don't need to modify anything in this file.
+ *
+ * In fact, unless you just thought "fuck you, yes I do" to that, you don't
+ *  need to modify anything in here.
+ *
+ * All functions here should be preceded by "TGT_".
+ *
+ * All functions and constants here should ONLY be used by arc_target_user.h.
+ *
+ * Nothing in arc_target_user.h should ever directly access TargetData.
+ *
+ * ===========================================================================
+ */
+
+// Important notes:
+//
+// - Column masks can only handle up to 32 columns. Should you exceed this amount
+//   of columns, you'll need to expand TGT_SetDataByArray to allow multiple
+//   column masks - specifically, (T_COLS/32) column masks.
+// 
+// - Switch +FRIENDLY to +NEVERTARGET once Zandronum 3.0 comes out.
+
+
+// How functions in here fit together:
+//
+// int TGT_Find(int tid)
+//  - Given a TID, find the target data row it occupies.
+//
+//  Returns: Row index for TID, -1 if nonexistent or if TID was 0.
+//
+//
+// int TGT_Add(int tid)
+//  - Add a given TID to the target data table.
+//
+//  Returns: Row index added to if added, negative row index if already present,
+//           T_ROWS if TID was 0.
+//
+//
+// int TGT_Del(int row)
+//  - Delete a given row in the target data table.
+//
+//  Returns: 0 if delete successful, 1 if row is out of bounds.
+//
+//
+// int TGT_DelTID(int tid)
+//  - Delete a given TID from the target data table.
+//
+//  Returns: 0 if delete successful, 1 if TID not found, 2 if TID was 0, 3 if
+//           things went horribly wrong and you should start panicking.
+//
+//
+// int TGT_SetData(int row, int column, int val)
+//  - Set a column in the target data table to a given value for the given row.
+//    Setting T_TID is expliclty disallowed.
+//
+//  Returns: 0 if set properly, 1 if row was out of range, 3 if the column to be
+//           set was out of range, 4 if the column was T_TID.
+//
+//
+// int TGT_SetDataByTID(int tid, int column, int val)
+//  - Set a column in the target data table to a given value for the given TID.
+//    Setting T_TID is expliclty disallowed.
+//
+//  Returns: 0 if set properly, 1 if TID not found, 2 if TID was 0, 3 if the
+//           column to be set was out of range, 4 if the column was T_TID.
+//
+//
+// int TGT_DataArray[T_COLS];
+//  - Data array read by TGT_SetDataByArray according to the column mask passed
+//    to it. Same layout as TargetData columns.
+//
+// int TGT_SetDataByArray(int row, int colmask)
+//  - Sets an entire row of target data, save T_TID, for the given row based off
+//    of a given column mask and the values of TGT_DataArray. This exists because
+//    setting an entire row at once is quicker than using TGT_SetDataByTID repeatedly
+//    when you need it.
+//
+//  Returns: 0 if columns were set properly, 1 if row was out of range.
+//
+//
+// int TGT_FlipFriend(int row, int state)
+//  - Gives friendliness/unfriendliness item to the TID on the given row.
+//
+//  Returns: 0 if friendliness state was set properly, 1 if row was out of bounds.
+//
+//
+// int TGT_ApplySlots(int row)
+//  - Give out friendliness/unfriendliness items to given row based off slot status
+//    in the target data. Only gives them if status changed.
+//
+//  Returns: -1 if TID was not found, 0 (false) if target at original friendliness,
+//           1 (true) if set to opposite friendliness, 2 is TID is 0, and 3 if
+//           it's time to start panicking again.
+//
+//
+// int TGT_Prune(void)
+//  - Clear out any targets that have no slots set on them.
+//
+//  Returns: amount of targets that got pruned.
+//
+//
+// int TGT_Clear(void)
+//  - Clear out TargetData entirely.
+//
+//  Returns: amount of targets that *were* in TargetData.
+//
+//
+//
+// 
+// If anything here contradicts the definitions above the functions themselves,
+//  the definitions above the functions take precedence.
+
+
+// TARGETSLOTS defines how many slots are defined for targeting purposes.
+//
+// Target slots are used to determine whether an enemy should be marked +FRIENDLY
+// or -FRIENDLY. If even one of them is set, the target's friendliness is flipped.
+// If all of them are unset, it's flipped back to what it was normally.
+//
+// The reason this is necessary is so that the arc won't pick up unwanted targets
+// multiple times with A_LookEx.
+//
+// The reason why this complicated system is necessary is because the last time I
+// tried this, friendliness tended to break. A lot. It probably still will, since
+// dead actors don't show up on ThingCount and TIDs can be shared with actors,
+// but there really isn't much of anything I can do about that.
+//
+// I hate TIDs. I hate juggling them around. I hate what they do to my code.
+// I hate the unexplainable bugs that arise from them, the inconsistencies in
+// how they're handled, how in the 17 years that ZDoom has had ACS there has been
+// no better solution made, how I can't even be goddamn sure that the TID I assign
+// to an actor will be *unique* or even *be there* next tic. I hate the fact that
+// by changing a TID on *anything*, I am breaking multiple maps out there. I hate
+// the dipshit developers who have refused to put in anything better for the past
+// 17 years. The whole goddamn system can go to hell.
+//
+// Just once - just fucking once - I want my code to not fall apart into a pile
+// of special cases and incomprehensible gibberish the moment it runs into them.
+// I want to be sure that I'm working with the right actor, that I'm not
+// accidentally moving some random demon or teleporter halfway across the map.
+// 
+// Is that seriously so much to ask for?
+//
+// Anyway, enough ranting. Here's what the slots are for.
+//
+// HIT      - used when a target's been hit by an arc
+//            can be unset automatically or manually, but always gets cleared
+//            every tic, at some point during the tic
+//
+// CHECKED  - used when looking for targets - this makes them untargetable so
+//            A_LookEx doesn't get stuck on them during an arc cycle
+
+#define TARGETSLOTS 2
+
+#define TSLOT_HIT       0
+#define TSLOT_CHECKED   1
+
+
+
+// Columns for TargetData!
+//
+// T_ROWS is how many targets can be stored. It's high so things don't go bad.
+//
+// T_COLS just defines how many columns there are.
+//
+// T_TID corresponds to the TID of the actor being looked at.
+//
+// T_ORIGTID corresponds to the original TID of the actor being looked at. It's
+//  normally equal to T_TID, since it's set to the same value as T_TID in TGT_Add,
+//  but it can be set elsewhere, presumably to avoid breaking maps horribly by
+//  changing a demon's TID from, say, 5 to 26001. Did I mention I hate TIDs?
+//
+// T_CURSTATE is there to mark whether the enemy's friendliness is currently flipped
+//  or not. Friendliness works in mysterious ways; apparently, making a monster
+//  -FRIENDLY multiple times adds to the monster counter every time.
+//  False means "original" and true means "flipped".
+//
+// T_ALREADYFRIEND marks whether the target started friendly or not. This is to
+//  reverse friendliness behaviour.
+//
+// T_SLOTSTART is the column at which the target slots above start at.
+//  From that column on, everything is a target slot.
+
+#define T_ROWS  65536
+#define T_COLS  (4 + TARGETSLOTS)
+
+#define T_TID               0
+#define T_ORIGTID           1
+#define T_CURSTATE          2
+#define T_ALREADYFRIEND     3
+#define T_SLOTSTART         4
+
+// These are for easier access to the slot columns.
+
+#define T_HIT               (T_SLOTSTART + TSLOT_HIT)
+#define T_CHECKED           (T_SLOTSTART + TSLOT_CHECKED)
+
+
+
+// These are the slot mask constants for TGT_SetDataByArray. Combine these with
+//  bitwise OR "|" to create the slot mask you want to use to set data with.
+//  These correspond with the above columns directly.
+//
+// TS_ALL specifies all of the columns.
+//
+// TS_TID isn't included because T_TID should not be changed, only read. The
+//  only two functions that should alter T_TID are TGT_Add and TGT_Del. If
+//  anything else alters them, duplicate entries could be made, with undefined
+//  behaviour.
+//
+// Alternately, just use the constants directly (1 << colIndex), but that's
+//  less readable.
+
+#define TS_ALL              ((1 << T_COLS) - 1)
+
+#define TS_ORIGTID          (1 << T_ORIGTID)
+#define TS_CURSTATE         (1 << T_CURSTATE)
+#define TS_ALREADYFRIEND    (1 << T_ALREADYFRIEND)
+
+#define TS_HIT              (1 << T_HIT)
+#define TS_CHECKED          (1 << T_CHECKED)
+
+
+
+// How many targets are currently stored in TargetData. This is used not
+//  just to keep track of how many targets there are, but also to keep
+//  TargetData contiguous - when a target is removed, the topmost target
+//  gets moved down into the now-empty slot. This way, when adding a target
+//  we can just slap it on the end without looking for an empty slot.
+//
+//  The less time spent traversing the array, the better.
+
+int TargetCount;
+
+
+
+// This stores the states of enemies that have been set +FRIENDLY as a result
+//  of the arc code. Don't touch it manually; down that road lies madness.
+//
+//  Trust me on that one. I'm rewriting this for a reason.
+
+int TargetData[T_ROWS][T_COLS];
+
+
+
+// Without this, TGT_Find takes O(n) time to run, where n's TargetCount. This
+//  quickly became unacceptable, given the amount of times TGT_Find gets called.
+//
+// When TGT_Add and TGT_Del manipulate rows, they'll go into TargetLookup and
+//  set index <TID> to the TID's row + 1.
+//
+// Why +1? World arrays initialize with everything set to 0 (I guess more
+//  accurately, each index is set to 0 when you first access it), but that 
+//  can't mean row 0 or else every uninitialized TID is on row 0 according to
+//  this. That's dumb. So now on a lookup, 0 gets shifted to -1, which is
+//  obviously impossible as an array index. 1 gets shifted to 0, so that means
+//  row 0.
+
+world int 45:TargetLookup[];
+
+
+
+// Find the index of a given TID in TargetData. If it doesn't exist,
+//  return -1.
+//
+// Used for many things, like making sure no duplicate TIDs end up in the
+//  array. +FRIENDLY is very fragile, so I'm taking precautions.
+//
+// There was a "int checkTid = TargetData[i][T_TID];" here. I took it out.
+//  Instruction count for looking dropped by 33%. Thanks, ACS.
+//
+// Special case: TID 0 always returns -1, because messing with TID 0 is horrible.
+
+function int TGT_Find(int tid)
+{
+    if (tid == 0) { return -1; }
+
+    int i;
+
+    // Do the fast way first.
+    
+    int lookup = TargetLookup[tid] - 1;
+    if (lookup == -1) { return -1; }
+    if (TargetData[lookup][T_TID] == tid) { return lookup; }
+
+    // Now the slow way, if the target lookup got the wrong index.
+
+    //for (i = 0; i < TargetCount; i++)
+    //{
+    //    if (tid == TargetData[i][T_TID]) { return i; }
+    //}
+
+    return -1;
+}
+
+
+
+// Add a target to TargetData, with no slots set yet, and return the
+//  index of the target. All other columns are set to appropriate values.
+//
+// If the target already exists in TargetData, return the index as a
+//  negative number. That way, the caller knows it already existed, but
+//  doesn't need to do another call to find it.
+//
+// This doesn't actually change any +FRIENDLY/-FRIENDLY flags: that is
+//  handled elsewhere.
+//
+// Special case: TID 0 returns T_ROWS as the index, since it's guaranteed
+//  to be out-of-bounds and no you're not messing with TID 0 seriously stop.
+
+function int TGT_Add(int tid)
+{
+    if (tid == 0) { return T_ROWS; }
+
+    int curRow = TGT_Find(tid);
+    if (curRow != -1) { return -curRow; }
+
+    curRow = TargetCount;
+
+    TargetData[curRow][T_TID]            = tid;
+    TargetData[curRow][T_ORIGTID]        = tid;
+    TargetData[curRow][T_CURSTATE]       = false;
+    TargetData[curRow][T_ALREADYFRIEND]  = GetActorProperty(tid, APROP_Friendly);
+
+    // Add +1 to the row because world arrays start at 0, and we want that to
+    //  mean "uninitialized", not "row 0".
+
+    TargetLookup[tid] = curRow + 1;
+
+    // Guess what? The above line might not actually grab the right actor!
+    //  GetActorProperty for TIDs with multiple actors assigned to them is
+    //  *completely undefined*!
+    //
+    // Yes, something as *checking friendliness* can cause bugs to appear.
+    
+    int i;
+
+    for (i = T_SLOTSTART; i < T_COLS; i++)
+    {
+        TargetData[curRow][i] = false;
+    }
+
+    TargetCount += 1;
+
+    return curRow;
+}
+
+
+
+// Clears a target by row, and pulls the top row down to the deleted slot to
+//  preserve contiguity.
+//
+// Note that we don't actually clear out the top row, we just copy its contents
+// over, since TGT_Add clears it anyway when it gets back to it.
+//
+// It also restores T_ORIGTID if it's different from T_TID, unless T_ORIGTID was 0.
+//
+// And just in case, we make sure that friendliness is set back to normal when
+//  we delete.
+//
+// Return values:
+//
+// - 0: Success, yay.
+//
+// - 1: Row given was out of bounds, and therefore nothing was done.
+
+function int TGT_Del(int row)
+{
+    if (row < 0 || row >= TargetCount) { return 1; }
+
+    // First, reset everything on the TID getting deleted.
+
+    TGT_FlipFriend(row, false);
+
+    int curTID  = TargetData[row][T_TID];
+    int origTID = TargetData[row][T_ORIGTID];
+
+    TargetLookup[curTID] = 0;
+
+    if ((origTID != 0) && (curTID != origTID)) { Thing_ChangeTID(curTID, origTID); }
+
+    // Now delete.
+
+    TargetCount -= 1;
+
+    // if we deleted the last row, we don't need to copy anything over
+    if (row != TargetCount)
+    {
+        int i;
+
+        for (i = 0; i < T_COLS; i++)
+        {
+            TargetData[row][i] = TargetData[TargetCount][i];
+        }
+
+        int newRowTID = TargetData[row][T_TID];
+        TargetLookup[newRowTID] = row + 1;
+    }
+
+    return 0;
+}
+
+
+
+// Clears a target by TID. Uses TGT_Del to do it.
+//
+// Return values:
+//
+// - 0: TID was found and deleted.
+//
+// - 1: The TID was not found, and nothing was deleted.
+//
+// - 2: The TID was 0.
+//
+// - 3: Something went horribly wrong: somehow, the TID was found, and it
+//      was out of bounds despite TGT_Find only looking within the bounds.
+//      If this happens, be scared.
+
+function int TGT_DelTID(int tid)
+{
+    if (tid == 0) { return 2; }
+
+    int curRow = TGT_Find(tid);
+    if (curRow == -1) { return 1; }
+
+    int result = TGT_Del(curRow);
+
+    // This should never happen.
+    if (result == 1) { return 3; }
+
+    return 0;
+}
+
+
+
+// Sets the given column of data on a row to the given value.
+//  This doesn't do any auto-pruning, nor does it apply anything.
+//  Setting T_TID is not allowed, because this could create duplicates.
+//
+// Return values:
+//
+// - 0: Column was set properly.
+//
+// - 1: Row was out of range.
+// 
+// - 2: Column was out of range.
+//
+// - 3: Column was T_TID, which is expliclty disallowed.
+
+function int TGT_SetData(int row, int column, int val)
+{
+    if (column == T_TID) { return 3; }
+    if (column < 0 || column >= T_COLS) { return 2; }
+    if (row < 0 || row >= TargetCount) { return 1; }
+
+    TargetData[row][column] = val;
+
+    return 0;
+}
+    
+
+// Sets the given column of data for a TID to the given value.
+//  This doesn't do any auto-pruning, nor does it apply anything.
+//  Setting T_TID is not allowed, because this could create duplicates.
+//
+// Return values:
+//
+// - 0: Column was set properly.
+//
+// - 1: TID was not found.
+//
+// - 2: TID was 0.
+// 
+// - 3: Column was out of range.
+//
+// - 4: Column was T_TID, which is expliclty disallowed.
+
+function int TGT_SetDataByTID(int tid, int column, int val)
+{
+    if (column == T_TID) { return 4; }
+    if (column < 0 || column >= T_COLS) { return 3; }
+    if (tid == 0) { return 2; }
+
+    int curRow = TGT_Find(tid);
+    if (curRow == -1) { return 1; }
+
+    TargetData[curRow][column] = val;
+
+    return 0;
+}
+
+
+
+// Sets data for a given row of data, given a column mask. See the TS_* constants
+//  at the top of the file for the column mask constants you should use.
+//
+// To use this function, you set the appropriate values in TGT_DataArray, then
+//  call the function with the proper column mask. 
+// 
+// For example, to set T_ORIGTID and T_CHECKED, you'd do something like this:
+//
+//      TGT_DataArray[T_ORIGTID] = 12345;
+//      TGT_DataArray[T_CHECKED] = true;
+//      TGT_SetDataByArray(row, TS_ORIGTID | TS_CHECKED);
+//
+// Again, you aren't allowed to change T_TID, to guarantee each TID is only found
+//  once in the target data.
+//
+// You *could* set index T_TID of TGT_DataArray, but it just gets ignored.
+//
+// Return values:
+//
+// - 0: Columns were set properly.
+//
+// - 1: Row was out of range.
+
+int TGT_DataArray[T_COLS];
+
+function int TGT_SetDataByArray(int row, int colmask)
+{
+    if (row < 0 || row >= TargetCount) { return 1; }
+
+    int i, checkmask;
+    for (i = 0; i < T_COLS; i++)
+    {
+        // preserve target data integrity
+        if (i == T_TID) { continue; }
+
+        // check if setting this column
+        checkmask = 1 << i;
+        if (!(colmask & checkmask)) { continue; }
+
+        TargetData[row][i] = TGT_DataArray[i];
+    }
+
+    return 0;
+}
+
+        
+
+// Flips the friendliness of the TID on the given row to the given state.
+//
+// Return values:
+//
+// - 0: Friendliness state was set properly.
+// 
+// - 1: Row was out of bounds, nothing changed.
+
+function int TGT_FlipFriend(int row, int state)
+{
+    if (row < 0 || row >= TargetCount) { return 1; }
+
+    int curState = TargetData[row][T_CURSTATE];
+    int curTID   = TargetData[row][T_TID];
+
+    if (curState != state)
+    {
+        // flip end state if started as friend
+
+        if (TargetData[row][T_ALREADYFRIEND])
+        {
+            if (state) { GiveActorInventory(curTID, "ArcUnfriendly", 1); }
+            else       { GiveActorInventory(curTID, "ArcFriendly",   1); }
+        }
+        else
+        {
+            if (state) { GiveActorInventory(curTID, "ArcFriendly",   1); }
+            else       { GiveActorInventory(curTID, "ArcUnfriendly", 1); }
+        }
+    }
+    
+    TargetData[row][T_CURSTATE] = state;
+
+    return 0;
+}
+
+
+
+// Checks the values of all the slots set on a TID, and if any of them are on, flips
+//  its friendliness.
+//
+// Return values:
+//
+// - -1: Row out of range.
+// 
+// -  0 (false): Target is set to original friendliness.
+//
+// -  1 (true):  Target is set to opposite friendliness.
+
+function int TGT_ApplySlots(int row)
+{
+    if (row < 0 || row >= TargetCount) { return -1; }
+
+    int flipFriendliness = false;
+    int i;
+
+    for (i = T_SLOTSTART; i < T_COLS; i++)
+    {
+        if (TargetData[row][i])
+        {
+            flipFriendliness = true;
+            break;
+        }
+    }
+
+    TGT_FlipFriend(row, flipFriendliness);
+    return flipFriendliness;
+}
+
+
+// Clears out any entries with no target slots set.
+//
+// We start from TargetCount - 1 because, since we pull rows from the top down,
+//  any row that gets pulled down from a deletion will have already been covered,
+//  simplifying the code just a little bit.
+//
+// Plus, if the top rows get deleted, this saves some cycles copying.
+//
+// Return value is the amount of targets that got pruned.
+
+function int TGT_Prune(void)
+{
+    int pruneCount = 0;
+    int curRow;
+
+    if (TargetCount == 0) { return 0; }
+
+    for (curRow = TargetCount - 1; curRow >= 0; curRow--)
+    {
+        int isFlipped = false;
+        int i;
+
+        for (i = T_SLOTSTART; i < T_COLS; i++)
+        {
+            if (TargetData[curRow][i])
+            {
+                isFlipped = true;
+                break;
+            }
+        }
+
+        if (!isFlipped)
+        {
+            TGT_Del(curRow);
+            pruneCount += 1;
+        }
+    }
+
+    return pruneCount;
+}
+
+// Just clear out the entirety of TargetData. We start from the top so that
+//  TGT_Del doesn't spend any time copying things over.
+//
+// Returns the amount of entries that *were* in TargetData.
+
+function int TGT_Clear(void)
+{
+    int ret = TargetCount;
+    int curRow;
+
+    if (ret == 0) { return 0; }
+
+    for (curRow = TargetCount - 1; curRow >= 0; curRow--)
+    {
+        TGT_Del(curRow);
+    }
+
+    return ret;
+}

+ 263 - 0
pk3/acs/arc_target_user.h

@@ -0,0 +1,263 @@
+/*
+ * ===========================================================================
+ * ======================= IMPORTANT THING LOOKEE HERE =======================
+ * 
+ * This is where everything in arc_target_core.h should be used.
+ *
+ * In fact, this is the *only* place where anything in arc_target_core.h
+ *  should be used.
+ *
+ * Here, you can specify whatever functions you need to manipulate thing
+ *  targeting junk, to use wherever else in the arc code.
+ *
+ * This file also serves as example code for how to use arc_target_core.h.
+ *
+ * All functions in here should start with "Target_".
+ * 
+ * All functions and constants in here can be freely accessed elsewhere.
+ *
+ * You should never need to modify TargetData directly. Reading from it is fine.
+ *
+ * ===========================================================================
+ */
+
+// Notes:
+//
+//  - Switch ThingCount to IsTIDUsed once Zandronum 3.0 comes out.
+//
+
+
+// How functions in here fit together:
+//
+// void Target_Debug(int past)
+//  - Prints out the TargetData array up to TargetCount + <past> to the console.
+
+
+// Column names for Target_Debug.
+
+int TargetColumnNames[T_COLS] =
+{
+    "TID           ",
+    "orig TID      ",
+    "state         ",
+    "started friend",
+    "[slot] hit    ",
+    "[slot] checked",
+};
+
+// Also for Target_Debug.
+
+#define TD_INT  0
+#define TD_BOOL 1
+#define TD_STR  2
+
+int TargetColumnTypes[T_COLS] = {TD_INT, TD_INT, TD_BOOL, TD_BOOL, TD_BOOL, TD_BOOL};
+
+
+// Sets slots on the current target according to two given slot masks
+//  (column masks, but only the slot columns are read). The first one
+//  turns any slot in its mask on. The second one turns any slot in its
+//  mask off. On overrides off.
+//
+// Then it applies slots.
+//
+// TID 0 is considered activator TID, rather than erroring.
+//
+// This function will automatically create a row for the target if it doesn't
+//  have one.
+//
+// Example:
+//
+// -----------------------------------------------------------------------------
+//
+// int slotOn  = 0;
+// int slotOff = 0;
+//
+// slotOn  |= TS_HIT;
+// slotOff |= TS_CHECKED;
+//
+// Target_SetSlots(12345, slotOn, slotOff);
+//
+// // T_HIT is now set on, and T_CHECKED is set off
+//
+// -----------------------------------------------------------------------------
+//
+// Return values:
+//  - 0: Slot mask set properly.
+
+function int Target_SetSlots(int tid, int slotmask_on, int slotmask_off)
+{
+    int i;
+
+    // The reason why we aren't doing defaultTID here is because we already
+    //  did that with Target_Add. Presumably their TID is already unique,
+    //  especially since arc execution should be contiguous
+    //  and occur instantly.
+
+    if (tid == 0) { tid = defaultTID(-1); }
+
+    int tidRow = TGT_Find(tid);
+
+    if (tidRow == -1)
+    {
+        tidRow = TGT_Add(tid);
+    }
+
+    int combinedMask;
+
+    for (i = T_SLOTSTART; i < T_COLS; i++)
+    {
+        int curMask = 1 << i;
+        int onState  = slotmask_on  & curMask;
+        int offState = slotmask_off & curMask;
+
+        // combinedMask is for either on or off
+        if (onState || offState)
+        {
+            combinedMask |= curMask;
+        }
+
+        if (onState)       { TGT_DataArray[i] = true; }
+        else if (offState) { TGT_DataArray[i] = false; }
+    }
+
+    TGT_SetDataByArray(tidRow, combinedMask);
+    TGT_ApplySlots(tidRow);
+
+    return 0;
+}
+
+// For when you can't be arsed with slot masks. This sets the specified slot
+//  mask to the specified state, then applies slots if it succeeded. Returns
+//  whatever TGT_SetDataByTID returns.
+//
+// This function will automatically create a row for the target if it doesn't
+//  have one.
+//
+//  Return values:
+//   - 0: Slot set properly.
+//
+//   - 3: Slot out of range.
+
+function int Target_SetSlot(int tid, int slot, int state)
+{
+    if (tid == 0) { tid = defaultTID(-1); }
+    if (slot < T_SLOTSTART || slot >= T_COLS) { return 2; }
+
+    int tidRow = TGT_Find(tid);
+
+    if (tidRow == -1)
+    {
+        tidRow = TGT_Add(tid);
+    }
+
+    int ret = TGT_SetData(tidRow, slot, state);
+
+    if (state)
+    {
+        TGT_FlipFriend(tidRow, true);
+    }
+    else
+    {
+        TGT_ApplySlots(tidRow);
+    }
+
+    return ret;
+}
+
+
+
+// Clear a slot for everyone and prune.
+//
+// Return value is the amount of pruned targets.
+//
+// Alternately, return value is -1 when the slot is out of range.
+
+function int Target_ClearSlot(int slot)
+{
+    if (slot < T_SLOTSTART || slot >= T_COLS) { return -1; }
+
+    int i;
+
+    for (i = 0; i < TargetCount; i++)
+    {
+        TGT_SetData(i, slot, false);
+    }
+
+    return TGT_Prune();
+}
+
+
+// It is literally TGT_Clear.
+//
+// Returns whatever TGT_Clear does.
+
+function int Target_Clear(void)
+{
+    return TGT_Clear();
+}
+
+
+
+// Logs debug info for TargetData, up to TargetCount + <past>.
+//
+// Example output format:
+//
+//  ---------------------------------------------------------------------------
+//
+//    == TARGETDATA DEBUG ==
+//
+//    == ROW 0 ==
+//
+//  TID            - 17000
+//  orig TID       - 0
+//  state          - true
+//  started friend - false
+//  [slot] hit     - false
+//  [slot] checked - true
+//
+//    ======
+//
+//  1 row found in TargetData (showing 1)
+//
+//    == DONE ==
+//
+//  ---------------------------------------------------------------------------
+
+function void Target_Debug(int past)
+{
+    int rowsToPrint = TargetCount + past;
+
+    Log(s:"\n  == TARGETDATA DEBUG ==\n");
+
+    int row;
+
+    for (row = 0; row < rowsToPrint; row++)
+    {
+        Log(s:"\n  == ROW ", d:row, s:" ==\n");
+
+        int col;
+
+        for (col = 0; col < T_COLS; col++)
+        {
+            int colVal  = TargetData[row][col];
+            int colType = TargetColumnTypes[col];
+            int colName = TargetColumnNames[col];
+
+            int colStr;
+
+            switch (colType)
+            {
+                case TD_INT:  colStr = StrParam(d:colVal); break;
+                case TD_BOOL: colStr = cond(colVal, "true", "false"); break;
+                case TD_STR:  colStr = colVal; break;
+            }
+
+            Log(s:colName, s:" - ", s:colStr);
+        }
+    }
+
+    Log(s:"\n  =====\n");
+    Log(d:TargetCount, s:" row", s:cond(TargetCount == 1, "", "s"), s:" found in TargetData (showing ", d:rowsToPrint, s:")");
+    Log(s:"\n  == DONE ==\n");
+}

+ 14 - 0
pk3/acs/arc_user.h

@@ -0,0 +1,14 @@
+script 193 (void)
+{
+    int myTID = defaultTID(-1);
+
+    SetActivator(0, AAPTR_TRACER);
+
+    int hisTID     = ActivatorTID();
+    int hisTID_new = defaultTID(-1);
+
+    SetActivator(myTID, AAPTR_TARGET);
+
+    Thing_Damage2(hisTID_new, 100, "Normal");
+    Thing_ChangeTID(hisTID_new, hisTID);
+}

+ 1 - 0
pk3/acs/arc_user_const.h

@@ -0,0 +1 @@
+// Put constant definitions for user functions/scripts here. IF YOU DARE.

File diff suppressed because it is too large
+ 1319 - 0
pk3/acs/commonFuncs.h


+ 367 - 0
pk3/decorate/core.dec

@@ -0,0 +1,367 @@
+// Everything in this file is required for the arc code to work properly.
+//  No other DECORATE files are required.
+
+
+
+// Core arc script number. This is also in acs/arc_const.h, so be sure
+//  to change it there as well.
+
+const int ARC_MAIN  = 191;
+const int ARC_CLEAR = 192;
+
+
+
+// This is the class that every arcing actor should be based off of,
+//  so that the arc ACS works properly.
+//
+// ========================= REALLY IMPORTANT GOD DAMN =========================
+//
+// (this is in acs/arc_main.h as well because it's THAT IMPORTANT)
+//
+// You REALLY REALLY should understand the process that arcing takes, so here it is,
+//  in very brief detail.
+// 
+// Pointers are in order {target, master, tracer}. "????" means "unset so far".
+//
+// "(D)" means we're running in DECORATE. "(A)" means we're running in ACS.
+//
+// Step 1  (D/A): call arcing script
+//  Pointers: {firer, ????, ????}
+//
+// Step 2  (A): ACS sends arcer to Arc_Query
+//  Pointers: {firer, ????, ????}
+//
+// Step 3  (D): Arc_Query's run, gives ArcItem_DoArc to itself if it wants to arc
+//  Pointers: {firer, ????, ????}
+//
+// Step 4  (A): If no ArcItem_DoArc, clear target data, send arcer to Arc_EndArc, and end
+//  Pointers: {firer, ????, ????}
+//
+// - IF NOT ENDED -
+//
+//      --- LOOP ---
+//
+//      Step 5 (A): Send arcer to Arc_Look
+//       Pointers: {firer, ????, ????}
+//
+//      Step 6 (D): Look around
+//       Pointers: {firer, ????, potential target}
+//
+//      Step 7 (A): Nothing in tracer pointer? Break loop
+//       Pointers: {firer, ????, potential target}
+//
+//      Step 8 (A): Something in tracer pointer? Process it, add T_CHECKED target slot to it
+//       Pointers: {firer, ????, potential target}
+//
+//      --- END LOOP (step 9 goes back to top of loop) ---
+//
+// Step 10 (A): Clear all T_CHECKED target slots
+//  Pointers: {firer, ????, now-irrelevant target}
+//
+// Step 11 (A): No target found? Clear target data, Stick last target (if any) in tracer field,
+//              send arcer to Arc_NoTarget, end
+//  Pointers: {firer, ????, last target (if any)}
+//
+// - IF NOT ENDED -
+//
+// Step 12 (A): Targets found? Calculate best one, stick it in tracer field
+//  Pointers: {firer, ????, best target}
+//
+// Step 13 (D): Do Arc_FoundTarget (now's the time to do damage)
+//  Pointers: {firer, ????, best target}
+//  
+// Step 14 (A): Stick T_HIT target slot on calculated best target (it is now +FRIENDLY)
+//  Pointers: {firer, ????, best target}
+//
+// Step 15 (A): Spawn next arcer object on best target, focus on that one now
+//  Pointers: {empty, empty, empty}
+//
+// Step 16 (A): Set up pointers on new arcer object
+//  Pointers: {firer, current arcer, current target}
+//
+// Step 17 (A): Send new arcer object to Arc_Spawn
+//
+//
+//
+// Now that you've read all that, there's another important detail: all arc
+//  state blocks MUST END on a state with a length of at least 1 tic. If the
+//  entire state block is 0 tics long, the actor will disappear, and bad things
+//  will happen. I don't know what bad things. But something will break.
+//
+// The exceptions are Arc_EndArc and Arc_NoTarget, because the entire arc chain
+//  ends with those. Arc_FoundTarget is *not* an exception, because each newly-
+//  spawned arc actor has its master pointer set to the previous arc actor.
+//  Don't break the chain.
+
+actor Arc_Base
+{
+    Projectile
+
+    +NOINTERACTION
+    +SERVERSIDEONLY
+    +LOOKALLAROUND
+    +FRIENDLY
+
+    // You won't see this actor online because of +SERVERSIDEONLY, so might
+    //  as well make it so you won't see it in single player either.
+    RenderStyle None
+
+    Radius 0
+    Height 0
+
+    // This is used for A_LookEx to specify just how far we can look.
+    var int user_lookDist;
+
+    States
+    {
+    // The arcing object just needs to exist for a tic, to make sure that
+    //  we can actually use the damn thing.
+    //
+    // Note that arcers spawned by the arc script do NOT have any time to
+    //  run Spawn states (unless you use NoDelay in ZDoom, I guess). You
+    //  can use this behaviour to run initialization stuff before firing
+    //  off the arc, without the need to create a new actor just for that.
+    //
+    // Pointer states here:
+    //  - Target: unchanged (typically original firer)
+    //  - Master: unchanged (typically empty)
+    //  - Tracer: unchanged (typically empty)
+
+      Spawn:
+        TNT1 A 1
+        stop
+
+
+    // The actor is sent to this state when spawned by the arcing script.
+    //  All initialization behaviour should occur here. You don't even
+    //  necessarily have to keep arcing!
+    //
+    // The previous arc actor has been put into this actor's AAPTR_MASTER
+    //  field by the ACS. That way, we can follow a chain all the way back
+    //  to the start!
+    //
+    // Pointer states here:
+    //  - Target: original firer
+    //  - Master: previous arcer
+    //  - Tracer: unchanged
+
+      Arc_Spawn:
+        TNT1 A 1
+        stop
+
+    
+    
+    // This could've been implemented in ACS, but the arc would've been
+    //  less flexible for it. Plus, this also means less of the arc is
+    //  in ACS, and I'm thankful for that.
+    //
+    // When the arc is done checking what it needs to check, let the ACS
+    //  run again by using a state tic length that's greater than 0.
+    //
+    // The ACS will check the actor's inventory for ArcItem_DoArc,
+    //  and if it's present will continue on with the arcing. Otherwise,
+    //  it stops and sends the actor to Arc_EndArc.
+    //
+    // Pointer states here:
+    //  - Target: unchanged (typically original firer)
+    //  - Master: unchanged (typically previous arcer)
+    //  - Tracer: unchanged
+
+      Arc_Query:
+        TNT1 A 1
+        stop
+
+
+
+    // Convenience state block for Arc_Query. You can just jump to this
+    //  and it'll work for saying "yes, I want to keep arcing".
+    //
+    // Pointer states here:
+    //  - Target: unchanged (typically original firer)
+    //  - Master: unchanged (typically previous arcer)
+    //  - Tracer: unchanged
+
+      Arc_KeepArcing:
+        TNT1 A 1 A_GiveInventory("ArcItem_DoArc")
+        stop
+
+
+
+    // Convenience state block for Arc_Query. You can just jump to this
+    //  and it'll work for saying "no, I don't want to keep arcing".
+    //
+    // Pointer states here:
+    //  - Target: unchanged (typically original firer)
+    //  - Master: unchanged (typically previous arcer)
+    //  - Tracer: unchanged
+
+      Arc_StopArcing:
+        TNT1 A 1
+        stop
+
+
+
+    // So we aren't arcing anymore. Yay. This is where you end up.
+    //
+    // The chain of arc scripts end here, and by default, all the targeting data
+    //  has been reset before we got here, so feel free to do whatever you want.
+    //
+    // Pointer states here:
+    //  - Target: unchanged (typically original firer)
+    //  - Master: unchanged (typically previous arcer)
+    //  - Tracer: unchanged
+    
+      Arc_EndArc:
+        TNT1 A 1
+        stop
+
+
+
+    // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+    // !! THIS SHOULD NOT BE MODIFIED IN SUBCLASSES !!
+    // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+    //
+    // Sent to this state to look around for targets. user_lookDist is set
+    //  beforehand.
+    //
+    // A_LookEx puts who it finds in target, so we need to move the target
+    //  pointer into the master pointer before we do anything.
+    //
+    // Pointer states here:
+    //  - Target: empty
+    //  - Master: original firer
+    //  - Tracer: previous arcer
+
+      Arc_Look:
+        TNT1 A 0 A_RearrangePointers(AAPTR_NULL, AAPTR_TARGET, AAPTR_MASTER)
+        TNT1 A 0 A_LookEx(LOF_NOSOUNDCHECK, 0, user_lookDist, 0, 0, "Arc_LookYes")
+        goto Arc_LookNo
+
+
+
+    // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+    // !! THIS SHOULD NOT BE MODIFIED IN SUBCLASSES !!
+    // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+    //
+    // This is a separate state block because we don't have LOF_NOJUMP.
+    // 
+    // Since the target of a projectile is actually its tracer, we sort that
+    //  out here.
+    //
+    // The code also makes sure not to lose track of the previous arc actor,
+    //  and puts it back into AAPTR_MASTER.
+    //
+    // Pointer states here:
+    //  - Target: original firer
+    //  - Master: previous arcer
+    //  - Tracer: whoever was found by A_LookEx, if anyone
+
+      Arc_LookYes:
+        TNT1 A 0 A_RearrangePointers(AAPTR_MASTER, AAPTR_TRACER, AAPTR_TARGET)
+        TNT1 A 0 A_TakeInventory("ArcItem_NoTarget")
+        TNT1 A 1
+        stop
+
+    
+      Arc_LookNo:
+        TNT1 A 0 A_RearrangePointers(AAPTR_MASTER, AAPTR_TRACER, AAPTR_TARGET)
+        TNT1 A 0 A_GiveInventory("ArcItem_NoTarget")
+        TNT1 A 1
+        stop
+
+
+
+    // Sent to this state when the ACS finds out we didn't actually find
+    //  a target. Arc_Look2 has run, and the ACS has moved the pointers around
+    //  so that our old target is back in AAPTR_TRACER.
+    //
+    // The chain of arc scripts end here, and by default, all the targeting data
+    //  has been reset before we got here, so feel free to do whatever you want.
+    //
+    // Pointer states here:
+    //  - Target: original firer
+    //  - Master: previous arcer
+    //  - Tracer: previous (final) arc target
+
+      Arc_NoTarget:
+        TNT1 A 1
+        stop
+
+    // Sent to this state when the ACS finds out we did find a target, before
+    //  it does anything with the object. Arc_Look2 has run, so pointers are as
+    //  they are coming out of Arc_Look2.
+    //
+    // The arc script won't do any more SetActorStates on this actor once it's
+    //  here, so *now* you can do whatever requires delays.
+    //
+    // BUT MAKE SURE THIS THING LASTS FOR AT LEAST ONE TIC. The next arcer has
+    //  its master field set to this arcer, so make sure it still exists.
+    //
+    // Pointer states here:
+    //  - Target: original firer
+    //  - Master: previous arcer
+    //  - Tracer: new arc target
+      
+      Arc_FoundTarget:
+        TNT1 A 1
+        stop
+    }
+}
+
+
+
+
+// These are counter items.
+
+actor Arc_Counter: Inventory
+{
+    Inventory.MaxAmount 0x7FFFFFFF
+    Inventory.Amount    1
+    +UNDROPPABLE
+}
+
+actor Arc_Boolean: Inventory
+{
+    Inventory.MaxAmount 1
+    Inventory.Amount    1
+    +UNDROPPABLE
+}
+
+// Used in the Arc_Query state of Arc_Base.
+actor ArcItem_DoArc:    Arc_Boolean {}
+
+actor ArcItem_NoTarget: Arc_Boolean {}
+
+
+// These two items are used in arc_target_core.h to toggle whether the given
+//  target has flipped friendliness or not. It flips so that A_LookEx can
+//  find other targets without getting hung up on one forever.
+
+actor ArcFriendly: CustomInventory
+{
+    States
+    {
+      Spawn:
+        TNT1 A 0
+        stop
+
+      Pickup:
+        TNT1 A 0 A_ChangeFlag("FRIENDLY", 1)
+        stop
+    }
+}
+
+actor ArcUnfriendly: CustomInventory
+{
+    States
+    {
+      Spawn:
+        TNT1 A 0
+        stop
+
+      Pickup:
+        TNT1 A 0 A_ChangeFlag("FRIENDLY", 0)
+        stop
+    }
+}
+

+ 32 - 0
pk3/decorate/projectile.dec

@@ -0,0 +1,32 @@
+actor ArcTest_JumpCounter: Arc_Counter {}
+
+actor ArcTest: Arc_Base
+{
+    RenderStyle Add
+
+    States
+    {
+      Spawn:
+        TNT1 A 0
+        TNT1 A 0 A_TakeFromTarget("ArcTest_JumpCounter")
+        TNT1 A 1 ACS_ExecuteWithResult(ARC_MAIN, 0)
+        stop
+
+      Arc_Spawn:
+        TNT1 A 1 ACS_ExecuteWithResult(ARC_MAIN, 0)
+        stop
+        
+      Arc_Query:
+        TNT1 A 0 A_JumpIfInTargetInventory("ArcTest_JumpCounter", 5, "Arc_StopArcing")
+        goto Arc_KeepArcing
+
+      Arc_FoundTarget:
+        PLSS A 0  A_GiveToTarget("ArcTest_JumpCounter")
+        PLSS A 70 ACS_ExecuteWithResult(193)
+        stop
+
+      Arc_NoTarget:
+        PLSS A 70 bright
+        stop
+    }
+}

BIN
pk3/maps/MAP01.wad


BIN
pk3/maps/MAP01.wad.bak


+ 30 - 0
pk3_old/DECORATE.dec

@@ -0,0 +1,30 @@
+#include "decorate/core.dec"
+#include "decorate/projectile.dec"
+
+actor ArcFriendly: CustomInventory
+{
+    States
+    {
+      Spawn:
+        TNT1 A 0
+        stop
+
+      Pickup:
+        TNT1 A 0 A_ChangeFlag("FRIENDLY", 1)
+        stop
+    }
+}
+
+actor ArcUnfriendly: CustomInventory
+{
+    States
+    {
+      Spawn:
+        TNT1 A 0
+        stop
+
+      Pickup:
+        TNT1 A 0 A_ChangeFlag("FRIENDLY", 0)
+        stop
+    }
+}

+ 13 - 0
pk3_old/LICENSE.txt

@@ -0,0 +1,13 @@
+           DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 
+                   Version 2, December 2004 
+
+Copyright (C) 2016 Ijon Tichy <jino.itchy@gmail.com>
+
+Everyone is permitted to copy and distribute verbatim or modified 
+copies of this license document, and changing it is allowed as long 
+as the name is changed. 
+
+           DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 
+  TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 
+
+ 0. You just DO WHAT THE FUCK YOU WANT TO.

+ 1 - 0
pk3_old/LOADACS.txt

@@ -0,0 +1 @@
+arc_clean

+ 95 - 0
pk3_old/acs/arc_angle.h

@@ -0,0 +1,95 @@
+// We need two pairs of angles, one for the current arcer so it faces the new
+//  arcer, and one for the new arcer so we know what direction it should face.
+//
+// The arcer's new angle will be somewhere between the direction the current
+//  arcer is facing, and the direction the new target is in. Where exactly is
+//  controlled by ADATA_INT_FACINGWEIGHT.
+//
+// A weight of 0 means "face the same direction as the current arcer". In effect,
+//  go straight forward.
+//
+// A weight of 1 means "face the target". In effect, bounce around like a fool.
+//
+// Note that you aren't limited to values between 0 and 1. If for some reason
+//  you want the new arc to *overshoot* the direction to face the target, you
+//  can - use a value greater than 1. If you want it to face further away from
+//  the target (effectively curving inward), use a value less than 0.
+// 
+
+#define ANG_TARGET  0
+#define ANG_NEW     1
+
+#define ANG_ANGLE   0
+#define ANG_PITCH   1
+
+int Arc_NewAngles[2][2];
+
+function void Arc_CalcNewAngle(int arcType, int arcerTID, int targetTID)
+{
+    int faceWeight = INT_ArcData[arcType][ADATA_INT_FACINGWEIGHT];
+    int faceFirer  = INT_ArcData[arcType][ADATA_INT_FACINGFIRER];
+
+    // Stealing the firer's direction instead of the current arcer's? Sure.
+    if (faceFirer)
+    {
+        SetActivator(arcerTID, AAPTR_TARGET);
+    }
+
+    int arcerAngle = GetActorAngle(0);
+    int arcerPitch = GetActorPitch(0);
+    
+    SetActivator(arcerTID);
+
+    int dX = GetActorX(targetTID) - GetActorX(arcerTID);
+    int dY = GetActorY(targetTID) - GetActorY(arcerTID);
+    int dZ = GetActorZ(targetTID) - GetActorZ(arcerTID);
+
+    // Remember trigonometry class!
+    //
+    // The angle of a 2D vector is the arc tangent defined by its X and Y values.
+    //
+    //      /|
+    //     / |           Y/X = tan o
+    //  Z /  | Y    atan Y/X =     o
+    //   /   |
+    //  /    |   And that's what VectorAngle is basically; the atan function.
+    // o_____'   Bam, angles.
+    //    X
+    //
+    // Also, a pitch looking up is negative, hence -dZ.
+
+    int targetAngle = VectorAngle(dX, dY);
+    int targetPitch = VectorAngle(magnitudeTwo_f(dX, dY), -dZ);
+    
+    // We aren't necessarily taking the shortest path from one angle to another
+    //  right now. If we're going from arcer angle 0 to target angle 0.9, the
+    //  below line suggests 0.9, not -0.1.
+    //
+    // Similarly, going from arcer angle 0.6 to target angle 0, the below line
+    //  suggests -0.6 and not 0.4.
+
+    int angleDiff = targetAngle - arcerAngle;
+
+    // This corrects that.
+    if (angleDiff < -0.5) { angleDiff += 1.0; }
+    if (angleDiff >  0.5) { angleDiff -= 1.0; }
+
+
+    // Pitches can only be between -0.25 and 0.25 anyway, so no big deal here.
+    int pitchDiff = targetPitch - arcerPitch;
+
+
+    // Get the in between...
+    int middleAngle = arcerAngle + FixedMul(faceWeight, angleDiff);
+    int middlePitch = arcerPitch + FixedMul(faceWeight, pitchDiff);
+
+
+    // And assign shit!
+    
+    Arc_NewAngles[ANG_TARGET][ANG_ANGLE] = targetAngle;
+    Arc_NewAngles[ANG_TARGET][ANG_PITCH] = targetPitch;
+    
+    Arc_NewAngles[ANG_NEW][ANG_ANGLE] = middleAngle;
+    Arc_NewAngles[ANG_NEW][ANG_PITCH] = middlePitch;
+}
+

+ 66 - 0
pk3_old/acs/arc_clean.c

@@ -0,0 +1,66 @@
+#include "zcommon.acs"
+#library "arc_clean"
+
+// I've forgotten half the crap in here. The function names hopefully explain enough.
+#include "commonFuncs.h"
+
+// Concepts you should be aware of:
+//
+// Targeting data:
+//  Without special handling, A_LookEx will always find the same target, and it'll
+//   be the first thing that a counterclockwise fan starting at due east hits,
+//   damn how far away it is (as long as it's within range at all). So this ACS
+//   has a system dedicated towards flipping the +FRIENDLY/-FRIENDLY flag on
+//   any targets found, so that A_LookEx will actually look past them.
+//
+//  The first iteration of this arc code handled them... haphazardly. It took a
+//   while to actually get it so that all the flags would be flipped properly,
+//   and oftentimes one or two targets would slip by, causing, say, demons to
+//   turn against their allies the moment you hit them with an arc.
+//
+//  I intend to avoid all of that this time.
+
+
+// arc_const.h:
+//   Stores constant definitions that you're meant to mess with.
+//   Script numbers, arc definitions, stuff like that.
+//   You can modify anything in this file, but NOT delete it.
+//
+// arc_user_const.h:
+//   It's like arc_const.h, but meant for stuff in arc_user.h.
+//   You can delete everything in here and the arc code will still work.
+//   Just make sure to delete everything in arc_user.h as well.
+//
+// arc_target_core.h:
+//   This defines the core functions for managing targeting data. Don't use
+//   these directly.
+//
+// arc_target_user.h:
+//   This defines the user functions for managing targeting data. These can
+//   be used directly.
+//
+// arc_look.h:
+//   The nitty-gritty of how the arc acquires targets. It jumps between ACS and
+//   DECORATE to get the job done, a messy but unavoidable fact.
+//
+// arc_angle.h:
+//   This entire file is dedicated to calculating the angle of new arcers, as
+//   well as the angle between an arcer and its target.
+//
+// arc_main.h:
+//   Where most of it happens. This is where the main arc script is, the one
+//   that handles shuffling pointers around, keeping the arc chain going, and
+//   all that stuff.
+//
+// arc_user.h:
+//   None of this is necessary for the arc stuff to run. Stick your auxillary
+//   scripts and functions and whatnot in here.
+
+#include "arc_const.h"
+#include "arc_user_const.h"
+#include "arc_target_core.h"
+#include "arc_target_user.h"
+#include "arc_look.h"
+#include "arc_angle.h"
+#include "arc_main.h"
+#include "arc_user.h"

BIN
pk3_old/acs/arc_clean.o


+ 58 - 0
pk3_old/acs/arc_const.h

@@ -0,0 +1,58 @@
+// All constant definitions that can be changed freely are here.
+//  Do not *delete* anything, though. All the definitions and arrays are
+//  necessary, and deleting them will cause compiling to inevitably fail.
+
+// Core arc script number. This is also in decorate/core.dec, so be sure
+//  to change it there as well.
+
+#define ARC_MAIN        191
+#define ARC_CLEAR       192
+
+
+
+// Arc types need to be defined in ACS, because there's no wayt to store all
+//  the information on DECORATE actors (namely strings), and plus that'd be
+//  ripe for breakage.
+
+#define ARCTYPES    1
+
+// Arcer properties. Due to ACS limitations, ints and strings have to be stored
+//  in different arrays.
+
+#define ARCDATA_INT 5
+#define ARCDATA_STR 1
+
+// Maximum distance arc can look.
+#define ADATA_INT_MAXRANGE      0
+
+// How much should the angle between the target and arcer matter?
+#define ADATA_INT_ANGLEWEIGHT   1
+
+// Which direction should the new arcer face? Towards the new target (1.0),
+//  in the same direction the current arc is facing (0), or in between?
+#define ADATA_INT_FACINGWEIGHT  2
+
+// Instead of the current arc, should we use the *firer's* angle?
+//  A_BFGSpray antics ahoy!
+#define ADATA_INT_FACINGFIRER   3
+
+// Should this arcer not automatically clear target data when it's done arcing?
+//      
+// This is meant for spawning multiple arcers at once that shouldn't overlap.
+//  Make damn sure you call ARC_CLEAR when you're done.
+#define ADATA_INT_MANUALCLEAR   4
+
+
+
+// Name of arc actor to be spawned on a found target.
+#define ADATA_STR_NEXTARCER 0
+
+str STR_ArcData[ARCTYPES][ARCDATA_STR] =
+{
+    {"ArcTest"},
+};
+
+int INT_ArcData[ARCTYPES][ARCDATA_INT] = 
+{
+    {320, 1.0, 0.5, false, false},
+};

+ 267 - 0
pk3_old/acs/arc_look.h

@@ -0,0 +1,267 @@
+// Simply looking for shit needed an entire file dedicated to it.
+
+// Copying here for self-reminder:
+//
+//      --- LOOP ---
+//
+//      Step 5 (A): Send arcer to Arc_Look
+//       Pointers: {firer, ????, ????}
+//
+//      Step 6 (D): Look around
+//       Pointers: {firer, ????, potential target}
+//
+//      Step 7 (A): Nothing in tracer pointer? Break loop
+//       Pointers: {firer, ????, potential target}
+//
+//      Step 8 (A): Something in tracer pointer? Process it, add T_CHECKED target slot to it
+//       Pointers: {firer, ????, potential target}
+//
+//      Step 9 (A): New target processed, decide if it's better than current best, if so keep it
+//       Pointers: {firer, ????, potential target}
+//
+//      --- END LOOP ---
+//
+// Step 10 (A): Clear all T_CHECKED target slots
+//  Pointers: {firer, ????, now-irrelevant target}
+
+int ALook_AngX, ALook_AngY, ALook_AngZ;
+
+function int Look_Loop(int arcType, int arcerTID)
+{
+    int bestTargetTID = 0;
+    int bestTargetScore = 0x7FFFFFFF;
+
+    int rangeStep = 0;
+    int rangeMax  = INT_ArcData[arcType][ADATA_INT_MAXRANGE];
+    int stopLooking = false;
+
+    int i = 0;
+    
+    // Precalculate to save cycles. It's used in Loop_CalcScore.
+    // 
+    // It's the vector of the angle the arc is facing.
+
+    int arcAngle = GetActorAngle(arcerTID);
+    int arcPitch = GetActorPitch(arcerTID);
+
+    ALook_AngX = FixedMul(cos(arcAngle), cos(arcPitch));
+    ALook_AngY = FixedMul(sin(arcAngle), cos(arcPitch));
+    ALook_AngZ =          sin(arcPitch);
+
+    while (!stopLooking)
+    {
+        rangeStep += 32;
+        rangeStep  = min(rangeStep, rangeMax);
+
+        SetUserVariable(arcerTID, "user_lookDist", rangeStep);
+
+        while (true)
+        {
+            SetActivator(arcerTID);
+
+            // Steps 5 and 6
+            SetActorState(0, "Arc_Look");
+
+            SetActivator(0, AAPTR_TRACER);
+
+            // Step 7
+            if (ClassifyActor(0) & ACTOR_WORLD)
+            {
+                break;
+            }
+
+            i++;
+            Log(s:"scoring target ", d:i);
+
+            // Step 8
+            int targetTID   = defaultTID(-1);
+            int targetScore = Loop_CalcScore(arcType, arcerTID, targetTID, rangeMax, bestTargetScore);
+
+            Target_SetSlot(targetTID, T_CHECKED, true);
+
+            // Step 9
+            if ((targetScore != -1) && (targetScore < bestTargetScore))
+            {
+                bestTargetTID   = targetTID;
+                bestTargetScore = targetScore;
+
+                rangeMax  = ftoi(targetScore);
+                rangeStep = min(rangeMax, rangeStep);
+                SetUserVariable(arcerTID, "user_lookDist", rangeStep);
+            }
+        }
+
+        if (rangeStep >= rangeMax)
+        {
+            stopLooking = true;
+        }
+
+        if (ftoi(bestTargetScore) < rangeStep)
+        {
+            stopLooking = true;
+            break;
+        }
+    }
+
+    Log(d:i, s:" targets processed");
+
+    // Step 10
+    Target_ClearSlot(T_CHECKED);
+
+    SetActivator(arcerTID);
+    return bestTargetTID;
+}
+
+
+
+
+// Given the TIDs of both the arcer and its potential target, as well as
+//  the arc type, calculate its targeting score. Lower is better. If the
+//  target's invalid, return -1 instead.
+//
+// We pass in currentBestScore for some score-calculating optimizations.
+//
+// Thing is with the score calculation is that it starts at the actor's distance
+//  from the arcer, and only goes up from there. So if the distance is higher than
+//  the current best score, there is no way it will beat the current best, so we
+//  can just return it as-is. Every calculation afterwards is meaningless.
+
+function int Loop_CalcScore(int arcType, int arcerTID, int targetTID, int maxRange, int currentBestScore)
+{
+    // First off, we check off the quick-to-calculate stuff.
+    
+    // Gather firer and target info.
+    SetActivator(arcerTID, AAPTR_Target);
+    int firerPln  = PlayerNumber();
+    int firerTID  = ActivatorTID();
+
+    SetActivator(targetTID);
+    int targetPln = PlayerNumber();
+
+    Log(s:"Checking ", n:0, s:" (tid ", d:targetTID, s:")");
+
+    SetActivator(arcerTID);
+
+    // Have we somehow targeted ourself? 
+    if (firerTID == targetTID) { return -1; }
+
+
+    // If the target and firer are players, we check player numbers and teams,
+    //  because friendliness checks aren't a reliable way of doing it.
+    if ((targetPln != -1) && (firerPln != -1))
+    {
+        int noTeamIndex;
+
+        // ZDoom has the "no team" index at 255. Zandronum has it at 4. DUMB.
+        SetDBEntry("arc", "iszand", 1);
+        int isZand = GetDBEntry("arc", "iszand");
+
+        if (isZand) { noTeamIndex = 4; }
+        else        { noTeamIndex = 255; }
+
+        // A -FRIENDLY arc will target the player who fired it. Rule that out.
+        if (firerPln == targetPln) { return -1; }
+
+        int firerTeam  = GetPlayerInfo(firerPln, PLAYERINFO_TEAM);
+        int targetTeam = GetPlayerInfo(firerPln, PLAYERINFO_TEAM);
+
+        // And let's make sure we're not team-killing.
+        if ((firerTeam != noTeamIndex) && (firerTeam == targetTeam)) { return -1; }
+    }
+    else
+    {
+        // Friendliness check - same friendliness flags? Not valid.
+
+        int arcerFriend  = GetActorProperty(firerTID,  APROP_Friendly);
+        int targetFriend = GetActorProperty(targetTID, APROP_Friendly);
+
+        if (arcerFriend == targetFriend)
+        {
+            return -1;
+        }
+    }
+
+    // At this point, we're pretty damn sure we have a valid target.
+    //  Now only distance checks will rule them out.
+
+    // Any score above this can be ruled out.
+    int rangeLimiter = min(itof(maxRange), currentBestScore);
+
+    int arcerX = GetActorX(arcerTID);
+    int arcerY = GetActorY(arcerTID);
+    int arcerZ = GetActorZ(arcerTID);
+
+    int targetX = GetActorX(targetTID);
+    int targetY = GetActorY(targetTID);
+    int targetZ = GetActorZ(targetTID) + GetActorViewHeight(targetTID);
+
+    int dX = targetX - arcerX;
+    int dY = targetY - arcerY;
+    int dZ = targetZ - arcerZ;
+
+    // dX and dY were already implicitly checked by A_LookEx, but not dZ,
+    //  so let's handle that to quickly rule out targets too high or low up.
+    if (abs(dZ) >= rangeLimiter) { return -1; }
+
+
+    // Well, that's all the fast checks out of the way. Time for slow stuff.
+
+    int distanceScore = magnitudeThree_f(dX, dY, dZ);
+
+    // If the distance is higher than the current best score, there's no way
+    //  it can beat it when adjusted, so disqualify it. Also, range limits.
+    if (distanceScore >= rangeLimiter) { return -1; }
+
+
+    // Time to factor in angle difference to the target's score.
+    //
+    // First off, we're just gonna grab this now. If it's 0,
+    //  it saves us a lot of time, because angle won't matter.
+
+    int angleWeight = INT_ArcData[arctype][ADATA_INT_ANGLEWEIGHT];
+    if (angleWeight <= 0) { return distanceScore; }
+
+
+    // Okay, now we need to weigh the angle.
+    // 
+    // Calculate difference between 3D angles.
+    //
+    //      acos(dot3(vec1, vec2) / (mag(vec1) * mag(vec2)))
+    //
+    // Since the arc angle vector's (vec1 here) magnitude always 1,
+    //  this just becomes:
+    //
+    //      acos(dot3(vec1, vec2) / mag(vec2))
+    //
+    //  angleDiff will never be above 0.5.
+
+    int angleDiff = acos(FixedDiv(dot3(ALook_AngX, ALook_AngY, ALook_AngZ, dX, dY, dZ), distanceScore));
+
+
+    // Now calculate the score. We can only go up from distanceScore, or else
+    //  some core assumptions for (very necessary) optimization are broken.
+    //
+    // Basically, this: if the best score is 128, anything at least 128 units
+    //  away cannot, under any circumstances, beat that score. This means we
+    //  can stop looking past 128 units because it's a complete waste of time,
+    //  which allows us to look in layers and stop exactly when it becomes
+    //  pointless.
+    //
+    // As for why we do FixedDiv, it's because of how as 1/x approaches x=0,
+    //  the rate of change gets faster and faster. For us, that means angles
+    //  get weighted much more harshly the further away they stray from the
+    //  zero-line.
+
+    int angleScore = FixedDiv(distanceScore, 1.0 - angleDiff);
+    Log(f:distanceScore, s:" -[", f:angleDiff, s:"]-> ", f:angleScore);
+
+    // Angle weight scale goes from 0 to 1.
+    if (angleWeight >= 1.0)
+    {
+        return angleScore;
+    }
+
+    
+    int weightedScore = distanceScore + FixedMul(angleWeight, angleScore - distanceScore);
+    return weightedScore;
+}

+ 163 - 0
pk3_old/acs/arc_main.h

@@ -0,0 +1,163 @@
+// ========================= REALLY IMPORTANT GOD DAMN =========================
+//
+// (this is in decorate/core.dec as well because it's THAT IMPORTANT)
+//
+// You REALLY REALLY should understand the process that arcing takes, so here it is,
+//  in very brief detail.
+// 
+// Pointers are in order {target, master, tracer}. "????" means "unset so far".
+//
+// "(D)" means we're running in DECORATE. "(A)" means we're running in ACS.
+//
+// Step 1  (D/A): call arcing script
+//  Pointers: {firer, ????, ????}
+//
+// Step 2  (A): ACS sends arcer to Arc_Query
+//  Pointers: {firer, ????, ????}
+//
+// Step 3  (D): Arc_Query's run, gives ArcItem_DoArc to itself if it wants to arc
+//  Pointers: {firer, ????, ????}
+//
+// Step 4  (A): If no ArcItem_DoArc, clear target data, send arcer to Arc_EndArc, and end
+//  Pointers: {firer, ????, ????}
+//
+// - IF NOT ENDED -
+//
+//      --- LOOP ---
+//
+//      Step 5 (A): Send arcer to Arc_Look
+//       Pointers: {firer, ????, ????}
+//
+//      Step 6 (D): Look around
+//       Pointers: {firer, ????, potential target}
+//
+//      Step 7 (A): Nothing in tracer pointer? Break loop
+//       Pointers: {firer, ????, potential target}
+//
+//      Step 8 (A): Something in tracer pointer? Process it, add T_CHECKED target slot to it
+//       Pointers: {firer, ????, potential target}
+//
+//      Step 9 (A): New target processed, decide if it's better than current best, if so keep it
+//       Pointers: {firer, ????, potential target}
+//
+//      --- END LOOP ---
+//
+// Step 10 (A): Clear all T_CHECKED target slots
+//  Pointers: {firer, ????, now-irrelevant target}
+//
+// Step 11 (A): No target? Clear target data, Stick last target (if any) in tracer field,
+//              send arcer to Arc_NoTarget, end
+//  Pointers: {firer, ????, last target (if any)}
+//
+// - IF NOT ENDED -
+//
+// Step 12 (A): Stick best target in tracer field
+//  Pointers: {firer, ????, best target}
+//
+// Step 13 (D): Do Arc_FoundTarget (now's the time to do damage)
+//  Pointers: {firer, ????, best target}
+//  
+// Step 14 (A): Stick T_HIT target slot on calculated best target (it is now +FRIENDLY)
+//  Pointers: {firer, ????, best target}
+//
+// Step 15 (A): Spawn next arcer object on best target, focus on that one now
+//  Pointers: {empty, empty, empty}
+//
+// Step 16 (A): Set up pointers on new arcer object
+//  Pointers: {firer, current arcer, current target}