diff --git a/doc/specs/udmf_srb2.txt b/doc/specs/udmf_srb2.txt
index b25ed1af38ee55b6c2ecf568c824520d800c58e7..93746e547b092a0a07748e9b6e5a716a5c42548f 100644
--- a/doc/specs/udmf_srb2.txt
+++ b/doc/specs/udmf_srb2.txt
@@ -33,7 +33,16 @@ II.A : Storage and Retrieval of Data
 II.B : Storage Within Archive Files
 -----------------------------------
 
-    No changes.
+In addition to the base specification SRB2 recognizes the following lumps
+between the TEXTMAP and ENDMAP lumps:
+
+    BEHAVIOR = contains compiled ACS code
+    ZNODES = Nodes (must be stored as extended GL nodes.)
+    BLOCKMAP = blockmap. It is recommended not to include this lump in UDMF maps.
+    REJECT = reject table. It is recommended not to include this lump in UDMF maps.
+
+    Lumps starting with 'SCRIPT' are guaranteed to be ignored by SRB2 so they
+    can be used to store ACS and dialogue script sources.
 
 --------------------------------
 II.C : Implementation Dependence
@@ -45,6 +54,12 @@ The SRB2 engine only supports the following namespace:
 The engine is allowed to refuse maps with an unsupported namespace,
 or emit a warning.
 
+Additionally, maps may include a "version" field, represented as an integer.
+This must be increased by one whenever backwards-incompatible changes are introduced.
+
+The highest version currently supported by the engine is:
+    0
+
 =======================================
 III. Standardized Fields
 =======================================
@@ -86,6 +101,14 @@ Sonic Robo Blast 2 defines the following standardized fields:
       bouncy        = <bool>;     // Line is bouncy.
       transfer      = <bool>;     // In 3D floor sides, uses the sidedef properties of the control sector.
 
+      playercross   = <bool>;     // Special activates when a player crosses this line.
+      monstercross  = <bool>;     // Special activates when an enemy or boss crosses this line.
+      missilecross  = <bool>;     // Special activates when a projectile crosses this line.
+      playerpush    = <bool>;     // Special activates when a player pushes this line.
+      monsterpush   = <bool>;     // Special activates when an enemy or boss pushed this line.
+      impact        = <bool>;     // Special activates when a projectile hits this line.
+      repeatspecial = <bool>;     // Special is repeatable.
+
       alpha       = <float>;      // Translucency of this line. Default is 1.0
       renderstyle = <string>;     // Render style. Can be:
                                   // - "translucent"
@@ -203,6 +226,18 @@ Sonic Robo Blast 2 defines the following standardized fields:
       triggerline_plane       = <bool>;     // Trigger effects require a plane touch to be executed.
       triggerline_mobj        = <bool>;     // Trigger effects can be executed by non-pushable objects.
 
+      playerenter       = <bool>;     // Special activates when a player enters this sector.
+      playerfloor       = <bool>;     // Special activates when a player's feet touches the sector's floor.
+      playerceiling     = <bool>;     // Special activates when a player's head touches the sector's ceiling.
+      monsterenter      = <bool>;     // Special activates when an enemy or boss enters this sector.
+      monsterfloor      = <bool>;     // Special activates when an enemy or boss's feet touches the sector's floor.
+      monsterceiling    = <bool>;     // Special activates when an enemy or boss's head touches the sector's ceiling.
+      missileenter      = <bool>;     // Special activates when a missile enters this sector.
+      missilefloor      = <bool>;     // Special activates when a missile hits the sector's floor.
+      missileceiling    = <bool>;     // Special activates when a missile hits the sector's ceiling.
+      repeatspecial     = <bool>;     // Special is repeatable.
+      continuousspecial = <bool>;     // Special is activated continuously.
+
       invertprecip   = <bool>;         // Inverts the precipitation effect; if the sector is considered to be indoors,
                                        // precipitation is generated, and if the sector is considered to be outdoors,
                                        // precipitation is not generated.
@@ -296,6 +331,20 @@ Sonic Robo Blast 2 defines the following standardized fields:
       stringarg0 = <string>;  // String argument 0.
       stringarg1 = <string>;  // String argument 1.
 
+      special          = <integer>; // Special. Default = 0.
+      scriptarg0       = <integer>; // Special argument 0. Default = 0.
+      scriptarg1       = <integer>; // Special argument 1. Default = 0.
+      scriptarg2       = <integer>; // Special argument 2. Default = 0.
+      scriptarg3       = <integer>; // Special argument 3. Default = 0.
+      scriptarg4       = <integer>; // Special argument 4. Default = 0.
+      scriptarg5       = <integer>; // Special argument 5. Default = 0.
+      scriptarg6       = <integer>; // Special argument 6. Default = 0.
+      scriptarg7       = <integer>; // Special argument 7. Default = 0.
+      scriptarg8       = <integer>; // Special argument 8. Default = 0.
+      scriptarg9       = <integer>; // Special argument 9. Default = 0.
+      scriptstringarg0 = <string>;  // Special string argument 0.
+      scriptstringarg1 = <string>;  // Special string argument 1.
+
       comment = <string>; // A comment. Implementors should attach no special
                           // semantic meaning to this field.
    }
@@ -305,6 +354,10 @@ Sonic Robo Blast 2 defines the following standardized fields:
 Changelog
 =======================================
 
+1.1: 25.04.2024
+Added activation parameters for lines and sectors.
+Added special and script arguments for things.
+
 1.0: 19.02.2024
 Initial version.
 
diff --git a/extras/acs/srb2common.acs b/extras/acs/srb2common.acs
new file mode 100644
index 0000000000000000000000000000000000000000..32117409569d5d4c338c15c11162eb92900aa561
--- /dev/null
+++ b/extras/acs/srb2common.acs
@@ -0,0 +1,4 @@
+// Copyright (C) 2024 Russell's Smart Interfaces
+
+#include "srb2special.acs"
+#include "srb2defs.acs"
diff --git a/extras/acs/srb2defs.acs b/extras/acs/srb2defs.acs
new file mode 100644
index 0000000000000000000000000000000000000000..895b8e7e41a7c73243a291271a81a07ee2f5f352
--- /dev/null
+++ b/extras/acs/srb2defs.acs
@@ -0,0 +1,357 @@
+// Copyright (C) 2024 Russell's Smart Interfaces
+
+#define TRUE                    1
+#define FALSE                   0
+#define ON                      1
+#define OFF                     0
+#define YES                     1
+#define NO                      0
+
+#define FRACUNIT                65536
+#define TICRATE                 35
+
+#define LINE_FRONT              0
+#define LINE_BACK               1
+
+#define SIDE_FRONT              0
+#define SIDE_BACK               1
+
+#define TEXTURE_TOP             0
+#define TEXTURE_MIDDLE          1
+#define TEXTURE_BOTTOM          2
+
+#define GAME_COOPERATIVE        0
+#define GAME_COMPETITION        1
+#define GAME_RACE               2
+#define GAME_MATCH              3
+#define GAME_TEAMMATCH          4
+#define GAME_TAG                5
+#define GAME_HIDEANDSEEK        6
+#define GAME_CTF                7
+
+// Actor properties you can get/set -----------------------------------------
+
+#define APROP_X             0
+#define APROP_Y             1
+#define APROP_Z             2
+#define APROP_Type          3
+#define APROP_Angle         4
+#define APROP_Pitch         5
+#define APROP_Roll          6
+#define APROP_SpriteRoll    7
+#define APROP_Frame         8
+#define APROP_Sprite        9
+#define APROP_Sprite2       10
+#define APROP_RenderFlags   11
+#define APROP_SpriteXScale  12
+#define APROP_SpriteYScale  13
+#define APROP_SpriteXOffset 14
+#define APROP_SpriteYOffset 15
+#define APROP_FloorZ        16
+#define APROP_CeilingZ      17
+#define APROP_Radius        18
+#define APROP_Height        19
+#define APROP_MomX          20
+#define APROP_MomY          21
+#define APROP_MomZ          22
+#define APROP_Tics          23
+#define APROP_State         24
+#define APROP_Flags         25
+#define APROP_Flags2        26
+#define APROP_ExtraFlags    27
+#define APROP_Skin          28
+#define APROP_Color         29
+#define APROP_Health        30
+#define APROP_MoveDir       31
+#define APROP_MoveCount     32
+#define APROP_ReactionTime  33
+#define APROP_Threshold     34
+#define APROP_LastLook      35
+#define APROP_Friction      36
+#define APROP_MoveFactor    37
+#define APROP_Fuse          38
+#define APROP_WaterTop      39
+#define APROP_WaterBottom   40
+#define APROP_Scale         41
+#define APROP_DestScale     42
+#define APROP_ScaleSpeed    43
+#define APROP_ExtraValue1   44
+#define APROP_ExtraValue2   45
+#define APROP_CustomVal     46
+#define APROP_CustomValMem  47
+#define APROP_Colorized     48
+#define APROP_Mirrored      49
+#define APROP_ShadowScale   50
+#define APROP_DispOffset    51
+#define APROP_Target        52
+#define APROP_Tracer        53
+#define APROP_HNext         54
+#define APROP_HPrev         55
+
+// Line properties
+
+#define LINEPROP_Flags      0
+#define LINEPROP_Alpha      1
+#define LINEPROP_Style      2
+#define LINEPROP_Activation 3
+#define LINEPROP_Action     4
+#define LINEPROP_Arg0       5
+#define LINEPROP_Arg1       6
+#define LINEPROP_Arg2       7
+#define LINEPROP_Arg3       8
+#define LINEPROP_Arg4       9
+#define LINEPROP_Arg5       10
+#define LINEPROP_Arg6       11
+#define LINEPROP_Arg7       12
+#define LINEPROP_Arg8       13
+#define LINEPROP_Arg9       14
+#define LINEPROP_Arg0Str    15
+#define LINEPROP_Arg1Str    16
+
+// Side properties
+
+#define SIDEPROP_XOffset       0
+#define SIDEPROP_YOffset       1
+#define SIDEPROP_TopTexture    2
+#define SIDEPROP_BottomTexture 3
+#define SIDEPROP_MidTexture    4
+#define SIDEPROP_RepeatCount   5
+
+// Sector properties
+
+#define SECPROP_FloorHeight          1
+#define SECPROP_CeilingHeight        2
+#define SECPROP_FloorPic             3
+#define SECPROP_CeilingPic           4
+#define SECPROP_LightLevel           5
+#define SECPROP_FloorLightLevel      6
+#define SECPROP_CeilingLightLevel    7
+#define SECPROP_FloorLightAbsolute   8
+#define SECPROP_CeilingLightAbsolute 9
+#define SECPROP_Flags                10
+#define SECPROP_SpecialFlags         11
+#define SECPROP_Gravity              12
+#define SECPROP_Activation           13
+#define SECPROP_Action               14
+#define SECPROP_Arg0                 15
+#define SECPROP_Arg1                 16
+#define SECPROP_Arg2                 17
+#define SECPROP_Arg3                 18
+#define SECPROP_Arg4                 19
+#define SECPROP_Arg5                 20
+#define SECPROP_Arg6                 21
+#define SECPROP_Arg7                 22
+#define SECPROP_Arg8                 23
+#define SECPROP_Arg9                 24
+#define SECPROP_Arg0Str              25
+#define SECPROP_Arg1Str              26
+
+// Render Styles ------------------------------------------------------------
+
+#define STYLE_Copy            0   // Just copy the image to the screen
+#define STYLE_Translucent     1   // Draw translucent
+#define STYLE_Add             2   // Draw additive
+#define STYLE_Subtract        3   // Draw subtractive
+#define STYLE_ReverseSubtract 4   // Draw reverse subtractive
+#define STYLE_Modulate        5   // Draw multiplicative
+#define STYLE_Fog             7   // Draw reverse subtractive
+
+// Line activation flags
+
+#define SPAC_None           0
+#define SPAC_Repeat         1       // repeatable
+#define SPAC_Cross          2       // when player crosses line
+#define SPAC_MCross         4       // when monster crosses line
+#define SPAC_PCross         8       // when projectile crosses line
+#define SPAC_Push           16      // when player pushes line
+#define SPAC_MPush          32      // monsters can push
+#define SPAC_Impact         64      // when projectile hits line
+
+// Sector activation flags
+
+#define SECSPAC_None                   0
+#define SECSPAC_OnceSpecial            1         // special action is activated once
+#define SECSPAC_RepeatSpecial          2         // special action is repeatable
+#define SECSPAC_ContinuousSpecial      4         // special action is activated continously
+#define SECSPAC_Enter                  8         // when a player enters this sector
+#define SECSPAC_Floor                  16        // when a player touches the floor of this sector
+#define SECSPAC_Ceiling                32        // when a player touches the ceiling of this sector
+#define SECSPAC_EnterMonster           64        // when an enemy enters this sector
+#define SECSPAC_FLOORMonster           128       // when an enemy touches the floor of this sector
+#define SECSPAC_CeilingMonster         256       // when an enemy touches the ceiling of this sector
+#define SECSPAC_EnterMissile           512       // when a projectile enters this sector
+#define SECSPAC_FloorMissile           1024      // when a projectile touches the floor of this sector
+#define SECSPAC_CeilingMissile         2048      // when a projectile touches the ceiling of this sector
+
+// Teams -----------------------------------------------------------
+
+#define NO_TEAM                 0
+#define TEAM_RED                1
+#define TEAM_BLUE               2
+
+// Bot types
+#define BOT_NONE    0
+#define BOT_2PAI    1
+#define BOT_2PHUMAN 2
+#define BOT_MPAI    3
+
+// Colors ----------------------------------------------------------
+
+#define SKINCOLOR_NONE 0
+#define SKINCOLOR_WHITE 1
+#define SKINCOLOR_BONE 2
+#define SKINCOLOR_CLOUDY 3
+#define SKINCOLOR_GREY 4
+#define SKINCOLOR_SILVER 5
+#define SKINCOLOR_CARBON 6
+#define SKINCOLOR_JET 7
+#define SKINCOLOR_BLACK 8
+#define SKINCOLOR_AETHER 9
+#define SKINCOLOR_SLATE 10
+#define SKINCOLOR_MOONSTONE 11
+#define SKINCOLOR_BLUEBELL 12
+#define SKINCOLOR_PINK 13
+#define SKINCOLOR_ROSEWOOD 14
+#define SKINCOLOR_YOGURT 15
+#define SKINCOLOR_LATTE 16
+#define SKINCOLOR_BROWN 17
+#define SKINCOLOR_BOULDER 18
+#define SKINCOLOR_BRONZE 19
+#define SKINCOLOR_SEPIA 20
+#define SKINCOLOR_ECRU 21
+#define SKINCOLOR_TAN 22
+#define SKINCOLOR_BEIGE 23
+#define SKINCOLOR_ROSEBUSH 24
+#define SKINCOLOR_MOSS 25
+#define SKINCOLOR_AZURE 26
+#define SKINCOLOR_EGGPLANT 27
+#define SKINCOLOR_LAVENDER 28
+#define SKINCOLOR_RUBY 29
+#define SKINCOLOR_CHERRY 30
+#define SKINCOLOR_SALMON 31
+#define SKINCOLOR_PEPPER 32
+#define SKINCOLOR_RED 33
+#define SKINCOLOR_CRIMSON 34
+#define SKINCOLOR_FLAME 35
+#define SKINCOLOR_GARNET 36
+#define SKINCOLOR_KETCHUP 37
+#define SKINCOLOR_PEACHY 38
+#define SKINCOLOR_QUAIL 39
+#define SKINCOLOR_FOUNDATION 40
+#define SKINCOLOR_SUNSET 41
+#define SKINCOLOR_COPPER 42
+#define SKINCOLOR_APRICOT 43
+#define SKINCOLOR_ORANGE 44
+#define SKINCOLOR_RUST 45
+#define SKINCOLOR_TANGERINE 46
+#define SKINCOLOR_TOPAZ 47
+#define SKINCOLOR_GOLD 48
+#define SKINCOLOR_SANDY 49
+#define SKINCOLOR_GOLDENROD 50
+#define SKINCOLOR_YELLOW 51
+#define SKINCOLOR_OLIVE 52
+#define SKINCOLOR_PEAR 53
+#define SKINCOLOR_LEMON 54
+#define SKINCOLOR_LIME 55
+#define SKINCOLOR_PERIDOT 56
+#define SKINCOLOR_APPLE 57
+#define SKINCOLOR_HEADLIGHT 58
+#define SKINCOLOR_CHARTREUSE 59
+#define SKINCOLOR_GREEN 60
+#define SKINCOLOR_FOREST 61
+#define SKINCOLOR_SHAMROCK 62
+#define SKINCOLOR_JADE 63
+#define SKINCOLOR_MINT 64
+#define SKINCOLOR_MASTER 65
+#define SKINCOLOR_EMERALD 66
+#define SKINCOLOR_SEAFOAM 67
+#define SKINCOLOR_ISLAND 68
+#define SKINCOLOR_BOTTLE 69
+#define SKINCOLOR_AQUA 70
+#define SKINCOLOR_TEAL 71
+#define SKINCOLOR_OCEAN 72
+#define SKINCOLOR_WAVE 73
+#define SKINCOLOR_CYAN 74
+#define SKINCOLOR_TURQUOISE 75
+#define SKINCOLOR_AQUAMARINE 76
+#define SKINCOLOR_SKY 77
+#define SKINCOLOR_MARINE 78
+#define SKINCOLOR_CERULEAN 79
+#define SKINCOLOR_DREAM 80
+#define SKINCOLOR_ICY 81
+#define SKINCOLOR_DAYBREAK 82
+#define SKINCOLOR_SAPPHIRE 83
+#define SKINCOLOR_ARCTIC 84
+#define SKINCOLOR_CORNFLOWER 85
+#define SKINCOLOR_BLUE 86
+#define SKINCOLOR_COBALT 87
+#define SKINCOLOR_MIDNIGHT 88
+#define SKINCOLOR_GALAXY 89
+#define SKINCOLOR_VAPOR 90
+#define SKINCOLOR_DUSK 91
+#define SKINCOLOR_MAJESTY 92
+#define SKINCOLOR_PASTEL 93
+#define SKINCOLOR_PURPLE 94
+#define SKINCOLOR_NOBLE 95
+#define SKINCOLOR_FUCHSIA 96
+#define SKINCOLOR_BUBBLEGUM 97
+#define SKINCOLOR_SIBERITE 98
+#define SKINCOLOR_MAGENTA 99
+#define SKINCOLOR_NEON 100
+#define SKINCOLOR_VIOLET 101
+#define SKINCOLOR_ROYAL 102
+#define SKINCOLOR_LILAC 103
+#define SKINCOLOR_MAUVE 104
+#define SKINCOLOR_EVENTIDE 105
+#define SKINCOLOR_PLUM 106
+#define SKINCOLOR_RASPBERRY 107
+#define SKINCOLOR_TAFFY 108
+#define SKINCOLOR_ROSY 109
+#define SKINCOLOR_FANCY 110
+#define SKINCOLOR_SANGRIA 111
+#define SKINCOLOR_VOLCANIC 112
+#define SKINCOLOR_SUPERSILVER1 113
+#define SKINCOLOR_SUPERSILVER2 114
+#define SKINCOLOR_SUPERSILVER3 115
+#define SKINCOLOR_SUPERSILVER4 116
+#define SKINCOLOR_SUPERSILVER5 117
+#define SKINCOLOR_SUPERRED1 118
+#define SKINCOLOR_SUPERRED2 119
+#define SKINCOLOR_SUPERRED3 120
+#define SKINCOLOR_SUPERRED4 121
+#define SKINCOLOR_SUPERRED5 122
+#define SKINCOLOR_SUPERORANGE1 123
+#define SKINCOLOR_SUPERORANGE2 124
+#define SKINCOLOR_SUPERORANGE3 125
+#define SKINCOLOR_SUPERORANGE4 126
+#define SKINCOLOR_SUPERORANGE5 127
+#define SKINCOLOR_SUPERGOLD1 128
+#define SKINCOLOR_SUPERGOLD2 129
+#define SKINCOLOR_SUPERGOLD3 130
+#define SKINCOLOR_SUPERGOLD4 131
+#define SKINCOLOR_SUPERGOLD5 132
+#define SKINCOLOR_SUPERPERIDOT1 133
+#define SKINCOLOR_SUPERPERIDOT2 134
+#define SKINCOLOR_SUPERPERIDOT3 135
+#define SKINCOLOR_SUPERPERIDOT4 136
+#define SKINCOLOR_SUPERPERIDOT5 137
+#define SKINCOLOR_SUPERSKY1 138
+#define SKINCOLOR_SUPERSKY2 139
+#define SKINCOLOR_SUPERSKY3 140
+#define SKINCOLOR_SUPERSKY4 141
+#define SKINCOLOR_SUPERSKY5 142
+#define SKINCOLOR_SUPERPURPLE1 143
+#define SKINCOLOR_SUPERPURPLE2 144
+#define SKINCOLOR_SUPERPURPLE3 145
+#define SKINCOLOR_SUPERPURPLE4 146
+#define SKINCOLOR_SUPERPURPLE5 147
+#define SKINCOLOR_SUPERRUST1 148
+#define SKINCOLOR_SUPERRUST2 149
+#define SKINCOLOR_SUPERRUST3 150
+#define SKINCOLOR_SUPERRUST4 151
+#define SKINCOLOR_SUPERRUST5 152
+#define SKINCOLOR_SUPERTAN1 153
+#define SKINCOLOR_SUPERTAN2 154
+#define SKINCOLOR_SUPERTAN3 155
+#define SKINCOLOR_SUPERTAN4 156
+#define SKINCOLOR_SUPERTAN5 157
diff --git a/extras/acs/srb2special.acs b/extras/acs/srb2special.acs
new file mode 100644
index 0000000000000000000000000000000000000000..cc6839a797bb6a0f5f55f2a947639c73757b704f
--- /dev/null
+++ b/extras/acs/srb2special.acs
@@ -0,0 +1,116 @@
+// Copyright (C) 2024 Russell's Smart Interfaces
+
+special
+	-1:GetLineProperty(2),
+	-2:SetLineProperty(3),
+	-4:GetSectorProperty(2),
+	-5:SetSectorProperty(3),
+	-7:GetSideProperty(2),
+	-8:SetSideProperty(3),
+	-10:GetThingProperty(2),
+	-11:SetThingProperty(3),
+	-100:strcmp(2,3),
+	-101:strcasecmp(2,3),
+	-300:CountEnemies(2),
+	-301:CountPushables(2),
+	-303:HaveUnlockable(1),
+	-304:PlayerSkin(0),
+	-305:GetObjectDye(0),
+	-306:PlayerEmeralds(0),
+	-307:PlayerLap(0),
+	-308:LowestLap(0),
+	-309:RingSlingerMode(0),
+	-310:TeamGame(0),
+	-311:RecordAttack(0),
+	-312:Thing_Dye(1),
+	-313:CaptureTheFlagMode(0),
+	-315:PlayerBot(0),
+	-316:ModeAttacking(0),
+	-317:NiGHTSAttack(0),
+	-320:PlayerExiting(0),
+	-500:CameraWait(1),
+	-503:SetLineRenderStyle(3),
+	-504:MapWarp(2),
+	-505:AddBot(1, 4),
+	-507:ExitLevel(0,1),
+	-508:MusicPlay(1,2),
+	-509:MusicStopAll(0,1),
+	-510:MusicRestore(0),
+	-512:MusicDim(1),
+	// -700:AddMessage(1),
+	// -701:AddMessageForPlayer(1),
+
+	// SRB2 linedef types (400-499)
+	// Not all are implemented
+	400:Floor_SetHeight(2),
+	401:Ceiling_SetHeight(2),
+	402:Light_ChangeToValue(2),
+	403:Floor_Move(2),
+	404:Ceiling_Move(2),
+	409:Sector_ChangeTag(3),
+	411:Plane_Stop(1),
+	// 412:Teleport(2,5), // to reimplement
+	416:Light_StartFlickering(3,5),
+	417:Light_StartPulsating(3,5),
+	418:Light_StartBlinking(3,6),
+	419:Light_StartBlinkingSynchronized(3,6),
+	420:Light_Fade(3,6),
+	421:StopLightingEffect(1),
+	// 422:SwitchToCutAwayView(2), // should be reimplemented to search by TID
+	// 423:ChangeSky(1,2), // already implemented
+	424:Weather_Change(1,2),
+	425:Thing_ChangeState(1),
+	426:Thing_Stop(0,1),
+	427:AwardScore(1),
+	428:Plat_StartMovement(1,2),
+	429:Sector_Crush(1,2),
+	432:Switch2DMode(0,1),
+	433:GravityFlip(0,2),
+	// 434:AwardPowerUp(2), // to reimplement
+	435:Plane_ChangeScrollerDirection(2),
+	436:FOF_Shatter(2),
+	437:Player_DisableControls(1,2),
+	438:Thing_ChangeSize(1),
+	439:Line_ChangeTextures(2,4),
+	440:StartMetalSonicRace(0),
+	441:ConditionSetTrigger(1),
+	443:CallLuaFunction(1),
+	444:Earthquake(1,3),
+	445:FOF_Disappear(2,3),
+	446:FOF_Crumble(2,3),
+	447:Colormap_Change(1,3),
+	448:Skybox_Change(2,4),
+	449:EnableBosses(1,2),
+	450:LinedefExecute(1),
+	451:LinedefExecuteRandom(2),
+	452:FOF_SetTranslucency(3),
+	453:FOF_Fade(4),
+	454:FOF_StopFading(2),
+	455:Colormap_Fade(3,4),
+	456:Colormap_StopFading(1),
+	// 457:Thing_TrackAngle(4,5), // should be reimplemented to search by TID
+	// 458:Thing_StopTrackingAngle(0),
+	// 459:StartConversation(), // to reimplement
+	460:AwardRings(2),
+	// 461:Thing_Spawn(2), // to reimplement
+	462:StopTimer(0),
+	// 463:Thing_Dye(1), // Reimplemented as SetObjectDye
+	464:TriggerEggCapsule(1,2),
+	// 466:SetLevelFailureState(0,1), // to reimplement
+	475:ACS_NamedExecute(1,10),
+	476:ACS_NamedExecuteAlways(1,10),
+	477:ACS_NamedSuspend(1),
+	478:ACS_NamedTerminate(1),
+	480:Polyobj_DoorSlide(0),
+	481:Polyobj_DoorSwing(0),
+	482:Polyobj_Move(0),
+	483:Polyobj_MoveOverride(0),
+	484:Polyobj_RotateRight(0),
+	485:Polyobj_RotateRightOverride(0),
+	486:Polyobj_RotateLeft(0),
+	487:Polyobj_RotateLeftOverride(0),
+	488:Polyobj_WaypointMove(0),
+	489:Polyobj_TurnInvisibleIntangible(0),
+	490:Polyobj_TurnVisibleTangible(0),
+	491:Polyobj_SetTranslucency(0),
+	492:Polyobj_FadeTranslucency(0);
diff --git a/extras/conf/udb/Includes/SRB222_linedefs.cfg b/extras/conf/udb/Includes/SRB222_linedefs.cfg
index e297f473fd46d2848f3835765988bb913147e84f..8f16d6c14e2969aba843a62308f7c68121f5882f 100644
--- a/extras/conf/udb/Includes/SRB222_linedefs.cfg
+++ b/extras/conf/udb/Includes/SRB222_linedefs.cfg
@@ -3413,6 +3413,294 @@ udmf
 				enum = "setadd";
 			}
 		}
+
+		475
+		{
+			title = "Script Execute";
+			id = "ACS_Execute";
+
+			arg0
+			{
+				str = true;
+				titlestr = "Script Name";
+			}
+
+			arg1
+			{
+				title = "Script Argument 1";
+			}
+
+			arg2
+			{
+				title = "Script Argument 2";
+			}
+
+			arg3
+			{
+				title = "Script Argument 3";
+			}
+
+			arg4
+			{
+				title = "Script Argument 4";
+			}
+
+			arg5
+			{
+				title = "Script Argument 5";
+			}
+
+			arg6
+			{
+				title = "Script Argument 6";
+			}
+
+			arg7
+			{
+				title = "Script Argument 7";
+			}
+
+			arg8
+			{
+				title = "Script Argument 8";
+			}
+
+			arg9
+			{
+				title = "Script Argument 9";
+			}
+
+			arg10
+			{
+				title = "Script Argument 10";
+			}
+
+			stringarg0
+			{
+				title = "Script String Argument 1";
+			}
+
+			stringarg1
+			{
+				title = "Script String Argument 2";
+			}
+		}
+
+		476
+		{
+			title = "Script Execute Always";
+			id = "ACS_ExecuteAlways";
+
+			arg0
+			{
+				str = true;
+				titlestr = "Script Name";
+			}
+
+			arg1
+			{
+				title = "Script Argument 1";
+			}
+
+			arg2
+			{
+				title = "Script Argument 2";
+			}
+
+			arg3
+			{
+				title = "Script Argument 3";
+			}
+
+			arg4
+			{
+				title = "Script Argument 4";
+			}
+
+			arg5
+			{
+				title = "Script Argument 5";
+			}
+
+			arg6
+			{
+				title = "Script Argument 6";
+			}
+
+			arg7
+			{
+				title = "Script Argument 7";
+			}
+
+			arg8
+			{
+				title = "Script Argument 8";
+			}
+
+			arg9
+			{
+				title = "Script Argument 9";
+			}
+
+			arg10
+			{
+				title = "Script Argument 10";
+			}
+
+			stringarg0
+			{
+				title = "Script String Argument 1";
+			}
+
+			stringarg1
+			{
+				title = "Script String Argument 2";
+			}
+		}
+
+		477
+		{
+			title = "Script Suspend";
+			id = "ACS_Suspend";
+
+			arg0
+			{
+				str = true;
+				titlestr = "Script Name";
+			}
+
+			arg1
+			{
+				title = "Script Argument 1";
+			}
+
+			arg2
+			{
+				title = "Script Argument 2";
+			}
+
+			arg3
+			{
+				title = "Script Argument 3";
+			}
+
+			arg4
+			{
+				title = "Script Argument 4";
+			}
+
+			arg5
+			{
+				title = "Script Argument 5";
+			}
+
+			arg6
+			{
+				title = "Script Argument 6";
+			}
+
+			arg7
+			{
+				title = "Script Argument 7";
+			}
+
+			arg8
+			{
+				title = "Script Argument 8";
+			}
+
+			arg9
+			{
+				title = "Script Argument 9";
+			}
+
+			arg10
+			{
+				title = "Script Argument 10";
+			}
+
+			stringarg0
+			{
+				title = "Script String Argument 1";
+			}
+
+			stringarg1
+			{
+				title = "Script String Argument 2";
+			}
+		}
+
+		478
+		{
+			title = "Script Terminate";
+			id = "ACS_Terminate";
+
+			arg0
+			{
+				str = true;
+				titlestr = "Script Name";
+			}
+
+			arg1
+			{
+				title = "Script Argument 1";
+			}
+
+			arg2
+			{
+				title = "Script Argument 2";
+			}
+
+			arg3
+			{
+				title = "Script Argument 3";
+			}
+
+			arg4
+			{
+				title = "Script Argument 4";
+			}
+
+			arg5
+			{
+				title = "Script Argument 5";
+			}
+
+			arg6
+			{
+				title = "Script Argument 6";
+			}
+
+			arg7
+			{
+				title = "Script Argument 7";
+			}
+
+			arg8
+			{
+				title = "Script Argument 8";
+			}
+
+			arg9
+			{
+				title = "Script Argument 9";
+			}
+
+			arg10
+			{
+				title = "Script Argument 10";
+			}
+
+			stringarg0
+			{
+				title = "Script String Argument 1";
+			}
+
+			stringarg1
+			{
+				title = "Script String Argument 2";
+			}
+		}
 	}
 
 	linedefexecpoly
diff --git a/extras/conf/udb/Includes/SRB222_misc.cfg b/extras/conf/udb/Includes/SRB222_misc.cfg
index 0b94a5a87a37a5ce7cf6a2a4c9e5799f7c738eab..097d3eb6aa915afb6bfc88516219120a2baac5b9 100644
--- a/extras/conf/udb/Includes/SRB222_misc.cfg
+++ b/extras/conf/udb/Includes/SRB222_misc.cfg
@@ -40,6 +40,21 @@ linedefflags_udmf
 	transfer = "Transfer Line";
 }
 
+linedefactivations_udmf
+{
+	repeatspecial
+	{
+		name = "Repeatable action";
+		istrigger = false;
+	}
+	playercross = "When player walks over";
+	playerpush = "When player bumps";
+	monstercross = "When enemy walks over";
+	monsterpush = "When enemies bumps";
+	missilecross = "When projectile crosses";
+	impact = "On projectile impact";
+}
+
 linedefrenderstyles
 {
 	translucent = "Translucent";
@@ -610,6 +625,13 @@ script = This lump is a text-based script. Specify the filename of the script co
 */
 udmfmaplumpnames
 {
+	BEHAVIOR
+	{
+		required = false;
+		nodebuild = false;
+		blindcopy = true;
+	}
+
 	ZNODES
 	{
 		required = false;
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 20caecf7bd6a65a20631318799016da319d3f13d..e96532d044fa3e5a7ea3b94be0d682928f4211e5 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -150,6 +150,7 @@ set(SRB2_CONFIG_DEV_BUILD OFF CACHE BOOL
 	"Compile a development build of SRB2.")
 
 add_subdirectory(blua)
+add_subdirectory(acs)
 add_subdirectory(netcode)
 
 # OS macros
@@ -326,7 +327,6 @@ target_compile_options(SRB2SDL2 PRIVATE
 		-Wno-absolute-value
 		-Wextra
 		-Wno-trigraphs
-		-Wconditional-uninitialized
 		-Wno-error=non-literal-null-conversion
 		-Wno-error=constant-conversion
 		-Wno-error=unused-but-set-variable
@@ -345,7 +345,6 @@ target_compile_options(SRB2SDL2 PRIVATE
 		-Wall
 		-Wextra
 		-Wno-trigraphs
-		-Wconditional-uninitialized
 	>
 
 	# C++, MSVC
diff --git a/src/Makefile b/src/Makefile
index 40037834d3b2831f92a9897ea51336060addfc24..aa79ad137064a29c108ff6d2033a926a0afe26d2 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -183,10 +183,11 @@ objdir:=$(makedir)/objs
 sources+=\
 	$(call List,Sourcefile)\
 	$(call List,blua/Sourcefile)\
+	$(call List,acs/Sourcefile)\
 	$(call List,netcode/Sourcefile)\
 
-depends:=$(basename $(filter %.c %.s,$(sources)))
-objects:=$(basename $(filter %.c %.s %.nas,$(sources)))
+depends:=$(basename $(filter %.c %.cpp,$(sources)))
+objects:=$(basename $(filter %.c %.cpp,$(sources)))
 
 depends:=$(depends:%=$(depdir)/%.d)
 
@@ -251,7 +252,6 @@ opts+=$(foreach v,$(passthru_opts),$(if $($(v)),-D$(v)))
 
 opts+=$(WFLAGS) $(CPPFLAGS) $(CFLAGS)
 libs+=$(LDFLAGS)
-asflags:=$(ASFLAGS) -x assembler-with-cpp
 
 cc=$(CC)
 
@@ -355,7 +355,7 @@ endif
 endef
 
 $(eval $(call _recipe,c))
-$(eval $(call _recipe,s,$(asflags)))
+$(eval $(call _recipe,cpp))
 
 # compiling recipe template
 # 1: target file suffix
@@ -368,7 +368,7 @@ $(objdir)/%.$(1) : %.$(2) | $$$$(@D)/
 endef
 
 $(eval $(call _recipe,o,c,$(cc) -c -o $$@ $$<))
-$(eval $(call _recipe,o,s,$(cc) $(asflags) -c -o $$@ $$<))
+$(eval $(call _recipe,o,cpp,$(cc) -c -o $$@ $$<))
 $(eval $(call _recipe,res,rc,$(windres) -i $$< -o $$@))
 
 _rm=$(.)$(rmrf) $(call Windows_path,$(1))
diff --git a/src/Makefile.d/platform.mk b/src/Makefile.d/platform.mk
index d9a2954f60c54b30c121c0467022c1707f720fc0..b59214e9f6273f77835a39fe9c3dbf41f4fd2139 100644
--- a/src/Makefile.d/platform.mk
+++ b/src/Makefile.d/platform.mk
@@ -43,7 +43,6 @@ platform=cygwin
 else ifdef MINGW
 ifdef MINGW64
 NONX86=1
-NOASM=1
 # MINGW64 should not necessarily imply X86_64=1,
 # but we make that assumption elsewhere
 # Once that changes, remove this
diff --git a/src/Makefile.d/versions.mk b/src/Makefile.d/versions.mk
index 7c130d90846ab6f4199eb618ee918fdb6999e5cc..7dee2b657a06fefcc41d4a57cc5ab38edcdd3569 100644
--- a/src/Makefile.d/versions.mk
+++ b/src/Makefile.d/versions.mk
@@ -38,7 +38,8 @@ ifdef GCC41
  WFLAGS+=-Wshadow
 endif
 #WFLAGS+=-Wlarger-than-%len%
- WFLAGS+=-Wpointer-arith -Wbad-function-cast
+ WFLAGS+=-Wpointer-arith
+#WFLAGS+=-Wbad-function-cast
 ifdef GCC45
 #WFLAGS+=-Wc++-compat
 endif
@@ -68,9 +69,10 @@ endif
 endif
 #WFLAGS+=-Wstrict-prototypes
 ifdef GCC40
- WFLAGS+=-Wold-style-definition
+#WFLAGS+=-Wold-style-definition
 endif
- WFLAGS+=-Wmissing-prototypes -Wmissing-declarations
+ WFLAGS+=-Wmissing-declarations
+#WFLAGS+=-Wmissing-prototypes
 ifdef GCC40
  WFLAGS+=-Wmissing-field-initializers
 endif
@@ -81,7 +83,7 @@ endif
 #WFLAGS+=-Wpacked
 #WFLAGS+=-Wpadded
 #WFLAGS+=-Wredundant-decls
- WFLAGS+=-Wnested-externs
+#WFLAGS+=-Wnested-externs
 #WFLAGS+=-Wunreachable-code
  WFLAGS+=-Winline
 ifdef DEBUGMODE
diff --git a/src/acs/CMakeLists.txt b/src/acs/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..ccf5fafee99657a58241896fe1838e1497459d44
--- /dev/null
+++ b/src/acs/CMakeLists.txt
@@ -0,0 +1,24 @@
+target_sources(SRB2SDL2 PRIVATE
+	environment.cpp
+	environment.hpp
+	thread.cpp
+	thread.hpp
+	call-funcs.cpp
+	call-funcs.hpp
+	stream.cpp
+	stream.hpp
+	interface.cpp
+	interface.h
+)
+
+target_include_directories(SRB2SDL2 PRIVATE vm) # This sucks
+
+# This breaks Apple Clang 14 compile. It should be totally
+# unecessary since even though vm/CMakeLists.txt sets
+# CMAKE_CXX_FLAGS, it is in a lower scope.
+#set(ACSVM_NOFLAGS ON)
+
+set(ACSVM_SHARED OFF)
+add_subdirectory(vm)
+
+target_link_libraries(SRB2SDL2 PRIVATE acsvm)
diff --git a/src/acs/Sourcefile b/src/acs/Sourcefile
new file mode 100644
index 0000000000000000000000000000000000000000..cbebfdb9dd4d0718608af31981c127567193c881
--- /dev/null
+++ b/src/acs/Sourcefile
@@ -0,0 +1,26 @@
+environment.cpp
+thread.cpp
+call-funcs.cpp
+stream.cpp
+interface.cpp
+vm/ACSVM/Action.cpp
+vm/ACSVM/Array.cpp
+vm/ACSVM/BinaryIO.cpp
+vm/ACSVM/CallFunc.cpp
+vm/ACSVM/CodeData.cpp
+vm/ACSVM/Environment.cpp
+vm/ACSVM/Error.cpp
+vm/ACSVM/Function.cpp
+vm/ACSVM/Init.cpp
+vm/ACSVM/Jump.cpp
+vm/ACSVM/Module.cpp
+vm/ACSVM/ModuleACS0.cpp
+vm/ACSVM/ModuleACSE.cpp
+vm/ACSVM/PrintBuf.cpp
+vm/ACSVM/Scope.cpp
+vm/ACSVM/Script.cpp
+vm/ACSVM/Serial.cpp
+vm/ACSVM/String.cpp
+vm/ACSVM/Thread.cpp
+vm/ACSVM/ThreadExec.cpp
+vm/ACSVM/Tracer.cpp
diff --git a/src/acs/acsvm.hpp b/src/acs/acsvm.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..849dca0aa852b58c3f18c74b5ac279a013757dc5
--- /dev/null
+++ b/src/acs/acsvm.hpp
@@ -0,0 +1,50 @@
+// SONIC ROBO BLAST 2
+//-----------------------------------------------------------------------------
+// Copyright (C) 2024 by Russell's Smart Interfaces
+// Copyright (C) 2024 by Sonic Team Junior.
+// Copyright (C) 2016 by James Haley, David Hill, et al. (Team Eternity)
+// Copyright (C) 2024 by Sally "TehRealSalt" Cochenour
+// Copyright (C) 2024 by Kart Krew
+//
+// This program is free software distributed under the
+// terms of the GNU General Public License, version 2.
+// See the 'LICENSE' file for more details.
+//-----------------------------------------------------------------------------
+/// \file  acsvm.hpp
+/// \brief ACSVM include file
+
+#ifndef __SRB2_ACSVM_HPP__
+#define __SRB2_ACSVM_HPP__
+
+#include <ACSVM/Action.hpp>
+#include <ACSVM/Array.hpp>
+#include <ACSVM/BinaryIO.hpp>
+#include <ACSVM/CallFunc.hpp>
+#include <ACSVM/Code.hpp>
+#include <ACSVM/CodeData.hpp>
+#include <ACSVM/CodeList.hpp>
+#include <ACSVM/Environment.hpp>
+#include <ACSVM/Error.hpp>
+#include <ACSVM/Function.hpp>
+#include <ACSVM/HashMap.hpp>
+#include <ACSVM/HashMapFixed.hpp>
+#include <ACSVM/ID.hpp>
+#include <ACSVM/Init.hpp>
+#include <ACSVM/Jump.hpp>
+#include <ACSVM/List.hpp>
+#include <ACSVM/Module.hpp>
+#include <ACSVM/PrintBuf.hpp>
+#include <ACSVM/Scope.hpp>
+#include <ACSVM/Script.hpp>
+#include <ACSVM/Serial.hpp>
+#include <ACSVM/Stack.hpp>
+#include <ACSVM/Store.hpp>
+#include <ACSVM/String.hpp>
+#include <ACSVM/Thread.hpp>
+#include <ACSVM/Tracer.hpp>
+#include <ACSVM/Types.hpp>
+#include <ACSVM/Vector.hpp>
+
+#include <Util/Floats.hpp>
+
+#endif //__SRB2_ACSVM_HPP__
diff --git a/src/acs/call-funcs.cpp b/src/acs/call-funcs.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..8418ed72b7dfc84e92470b5b9fddeb65f7b7d6cd
--- /dev/null
+++ b/src/acs/call-funcs.cpp
@@ -0,0 +1,3463 @@
+// SONIC ROBO BLAST 2
+//-----------------------------------------------------------------------------
+// Copyright (C) 2024 by Russell's Smart Interfaces
+// Copyright (C) 2024 by Sonic Team Junior.
+// Copyright (C) 2016 by James Haley, David Hill, et al. (Team Eternity)
+// Copyright (C) 2024 by Sally "TehRealSalt" Cochenour
+// Copyright (C) 2024 by Kart Krew
+//
+// This program is free software distributed under the
+// terms of the GNU General Public License, version 2.
+// See the 'LICENSE' file for more details.
+//-----------------------------------------------------------------------------
+/// \file  call-funcs.cpp
+/// \brief Action Code Script: CallFunc instructions
+
+#include <algorithm>
+#include <cctype>
+
+#include "acsvm.hpp"
+
+#include "../doomtype.h"
+#include "../doomdef.h"
+#include "../doomstat.h"
+
+#include "../d_think.h"
+#include "../p_mobj.h"
+#include "../p_tick.h"
+#include "../w_wad.h"
+#include "../m_misc.h"
+#include "../m_random.h"
+#include "../g_game.h"
+#include "../d_player.h"
+#include "../r_defs.h"
+#include "../r_state.h"
+#include "../p_polyobj.h"
+#include "../taglist.h"
+#include "../p_local.h"
+#include "../b_bot.h"
+#include "../info.h"
+#include "../deh_tables.h"
+#include "../fastcmp.h"
+#include "../hu_stuff.h"
+#include "../s_sound.h"
+#include "../r_textures.h"
+#include "../m_cond.h"
+#include "../r_skins.h"
+#include "../z_zone.h"
+#include "../s_sound.h"
+#include "../r_draw.h"
+#include "../r_fps.h"
+#include "../netcode/net_command.h"
+
+#include "call-funcs.hpp"
+
+#include "environment.hpp"
+#include "thread.hpp"
+#include "../cxxutil.hpp"
+
+using namespace srb2::acs;
+
+/*--------------------------------------------------
+	static bool ACS_GetMobjTypeFromString(const char *word, mobjtype_t *type)
+
+		Helper function for CallFunc_ThingCount. Gets
+		an object type from a string.
+
+	Input Arguments:-
+		word: The mobj class string.
+		type: Variable to store the result in.
+
+	Return:-
+		true if successful, otherwise false.
+--------------------------------------------------*/
+static bool ACS_GetMobjTypeFromString(const char *word, mobjtype_t *type)
+{
+	if (fastncmp("MT_", word, 3))
+	{
+		// take off the MT_
+		word += 3;
+	}
+
+	for (int i = 0; i < NUMMOBJFREESLOTS; i++)
+	{
+		if (!FREE_MOBJS[i])
+		{
+			break;
+		}
+
+		if (fastcmp(word, FREE_MOBJS[i]))
+		{
+			*type = static_cast<mobjtype_t>(static_cast<int>(MT_FIRSTFREESLOT) + i);
+			return true;
+		}
+	}
+
+	for (int i = 0; i < MT_FIRSTFREESLOT; i++)
+	{
+		if (fastcmp(word, MOBJTYPE_LIST[i] + 3))
+		{
+			*type = static_cast<mobjtype_t>(i);
+			return true;
+		}
+	}
+
+	return false;
+}
+
+/*--------------------------------------------------
+	static bool ACS_GetSFXFromString(const char *word, sfxenum_t *type)
+
+		Helper function for sound playing functions.
+		Gets a SFX id from a string.
+
+	Input Arguments:-
+		word: The sound effect string.
+		type: Variable to store the result in.
+
+	Return:-
+		true if successful, otherwise false.
+--------------------------------------------------*/
+static bool ACS_GetSFXFromString(const char *word, sfxenum_t *type)
+{
+	if (fastnicmp("SFX_", word, 4)) // made case insensitive
+	{
+		// take off the SFX_
+		word += 4;
+	}
+	else if (fastnicmp("DS", word, 2)) // made case insensitive
+	{
+		// take off the DS
+		word += 2;
+	}
+
+	for (int i = 0; i < NUMSFX; i++)
+	{
+		if (S_sfx[i].name && fasticmp(word, S_sfx[i].name))
+		{
+			*type = static_cast<sfxenum_t>(i);
+			return true;
+		}
+	}
+
+	return false;
+}
+
+/*--------------------------------------------------
+	static bool ACS_GetSpriteFromString(const char *word, spritenum_t *type)
+
+		Helper function for CallFunc_Get/SetThingProperty.
+		Gets a sprite from a string.
+
+	Input Arguments:-
+		word: The sprite string.
+		type: Variable to store the result in.
+
+	Return:-
+		true if successful, otherwise false.
+--------------------------------------------------*/
+static bool ACS_GetSpriteFromString(const char *word, spritenum_t *type)
+{
+	if (fastncmp("SPR_", word, 4))
+	{
+		// take off the SPR_
+		word += 4;
+	}
+
+	for (int i = 0; i < NUMSPRITES; i++)
+	{
+		if (fastncmp(word, sprnames[i], 4))
+		{
+			*type = static_cast<spritenum_t>(i);
+			return true;
+		}
+	}
+
+	return false;
+}
+
+/*--------------------------------------------------
+	static bool ACS_GetSprite2FromString(const char *word, playersprite_t *type)
+
+		Helper function for CallFunc_Get/SetThingProperty.
+		Gets a sprite2 from a string.
+
+	Input Arguments:-
+		word: The sprite2 string.
+		type: Variable to store the result in.
+
+	Return:-
+		true if successful, otherwise false.
+--------------------------------------------------*/
+static bool ACS_GetSprite2FromString(const char *word, playersprite_t *type)
+{
+	if (fastncmp("SPR2_", word, 5))
+	{
+		// take off the SPR2_
+		word += 5;
+	}
+
+	for (int i = 0; i < free_spr2; i++)
+	{
+		if (fastcmp(word, spr2names[i]))
+		{
+			*type = static_cast<playersprite_t>(i);
+			return true;
+		}
+	}
+
+	return false;
+}
+
+/*--------------------------------------------------
+	static bool ACS_GetStateFromString(const char *word, playersprite_t *type)
+
+		Helper function for CallFunc_Get/SetThingProperty.
+		Gets a state from a string.
+
+	Input Arguments:-
+		word: The state string.
+		type: Variable to store the result in.
+
+	Return:-
+		true if successful, otherwise false.
+--------------------------------------------------*/
+static bool ACS_GetStateFromString(const char *word, statenum_t *type)
+{
+	if (fastncmp("S_", word, 2))
+	{
+		// take off the S_
+		word += 2;
+	}
+
+	for (int i = 0; i < NUMMOBJFREESLOTS; i++)
+	{
+		if (!FREE_STATES[i])
+		{
+			break;
+		}
+
+		if (fastcmp(word, FREE_STATES[i]))
+		{
+			*type = static_cast<statenum_t>(static_cast<int>(S_FIRSTFREESLOT) + i);
+			return true;
+		}
+	}
+
+	for (int i = 0; i < S_FIRSTFREESLOT; i++)
+	{
+		if (fastcmp(word, STATE_LIST[i] + 2))
+		{
+			*type = static_cast<statenum_t>(i);
+			return true;
+		}
+	}
+
+	return false;
+}
+
+/*--------------------------------------------------
+	static bool ACS_GetSkinFromString(const char *word, INT32 *type)
+
+		Helper function for CallFunc_Get/SetThingProperty.
+		Gets a skin from a string.
+
+	Input Arguments:-
+		word: The skin string.
+		type: Variable to store the result in.
+
+	Return:-
+		true if successful, otherwise false.
+--------------------------------------------------*/
+static bool ACS_GetSkinFromString(const char *word, INT32 *type)
+{
+	INT32 skin = R_SkinAvailable(word);
+	if (skin == -1)
+		return false;
+
+	*type = skin;
+	return true;
+}
+
+/*--------------------------------------------------
+	static bool ACS_GetColorFromString(const char *word, skincolornum_t *type)
+
+		Helper function for CallFunc_Get/SetThingProperty.
+		Gets a color from a string.
+
+	Input Arguments:-
+		word: The color string.
+		type: Variable to store the result in.
+
+	Return:-
+		true if successful, otherwise false.
+--------------------------------------------------*/
+static bool ACS_GetColorFromString(const char *word, skincolornum_t *type)
+{
+	for (int i = 0; i < numskincolors; i++)
+	{
+		if (fastcmp(word, skincolors[i].name))
+		{
+			*type = static_cast<skincolornum_t>(i);
+			return true;
+		}
+	}
+
+	return false;
+}
+
+/*--------------------------------------------------
+	static bool ACS_CountThing(mobj_t *mobj, mobjtype_t type)
+
+		Helper function for CallFunc_ThingCount.
+		Returns whenever or not to add this thing
+		to the thing count.
+
+	Input Arguments:-
+		mobj: The mobj we want to count.
+		type: Type exclusion.
+
+	Return:-
+		true if successful, otherwise false.
+--------------------------------------------------*/
+static bool ACS_CountThing(mobj_t *mobj, mobjtype_t type)
+{
+	if (type == MT_NULL || mobj->type == type)
+	{
+		// Don't count dead monsters
+		if (mobj->info->spawnhealth > 0 && mobj->health <= 0)
+		{
+			// Note: Hexen checks for COUNTKILL.
+			// SRB2 does not have an equivalent, so I'm checking
+			// spawnhealth. Feel free to replace this condition
+			// with literally anything else.
+			return false;
+		}
+
+		// Count this object.
+		return true;
+	}
+
+	return false;
+}
+
+/*--------------------------------------------------
+	static bool ACS_ActivatorIsLocal(ACSVM::Thread *thread)
+
+		Helper function for many print functions.
+		Returns whenever or not the activator of the
+		thread is a display player or not.
+
+	Input Arguments:-
+		thread: The thread we're exeucting on.
+
+	Return:-
+		true if it's for a display player,
+		otherwise false.
+--------------------------------------------------*/
+static bool ACS_ActivatorIsLocal(ACSVM::Thread *thread)
+{
+	auto info = &static_cast<Thread *>(thread)->info;
+
+	if ((info != NULL)
+		&& (info->mo != NULL && P_MobjWasRemoved(info->mo) == false)
+		&& (info->mo->player != NULL))
+	{
+		return info->mo->player == &players[displayplayer];
+	}
+
+	return false;
+}
+
+/*--------------------------------------------------
+	static UINT32 ACS_SectorThingCounter(sector_t *sec, mtag_t thingTag, bool (*filter)(mobj_t *))
+
+		Helper function for CallFunc_CountEnemies
+		and CallFunc_CountPushables. Counts a number
+		of things in the specified sector.
+
+	Input Arguments:-
+		sec: The sector to search in.
+		thingTag: Thing tag to filter for. 0 allows any.
+		filter: Filter function, total count is increased when
+			this function returns true.
+
+	Return:-
+		Numbers of things matching the filter found.
+--------------------------------------------------*/
+static UINT32 ACS_SectorThingCounter(sector_t *sec, mtag_t thingTag, bool (*filter)(mobj_t *))
+{
+	UINT32 count = 0;
+
+	for (msecnode_t *node = sec->touching_thinglist; node; node = node->m_thinglist_next) // things touching this sector
+	{
+		mobj_t *mo = node->m_thing;
+
+		if (thingTag != 0 && mo->tid != thingTag)
+		{
+			continue;
+		}
+
+		if (mo->z > sec->ceilingheight
+			|| mo->z + mo->height < sec->floorheight)
+		{
+			continue;
+		}
+
+		if (filter(mo) == true)
+		{
+			count++;
+		}
+	}
+
+	return count;
+}
+
+/*--------------------------------------------------
+	static UINT32 ACS_SectorTagThingCounter(mtag_t sectorTag, sector_t *activator, mtag_t thingTag, bool (*filter)(mobj_t *))
+
+		Helper function for CallFunc_CountEnemies
+		and CallFunc_CountPushables. Counts a number
+		of things in the tagged sectors.
+
+	Input Arguments:-
+		sectorTag: The sector tag to search in.
+		activator: The activator sector to fall back on when sectorTag is 0.
+		thingTag: Thing tag to filter for. 0 allows any.
+		filter: Filter function, total count is increased when
+			this function returns true.
+
+	Return:-
+		Numbers of things matching the filter found.
+--------------------------------------------------*/
+static UINT32 ACS_SectorIterateThingCounter(sector_t *sec, mtag_t thingTag, bool (*filter)(mobj_t *))
+{
+	UINT32 count = 0;
+	boolean FOFsector = false;
+	size_t i;
+
+	if (sec == nullptr)
+	{
+		return 0;
+	}
+
+	// Check the lines of this sector, to see if it is a FOF control sector.
+	for (i = 0; i < sec->linecount; i++)
+	{
+		INT32 targetsecnum = -1;
+
+		if (sec->lines[i]->special < 100 || sec->lines[i]->special >= 300)
+		{
+			continue;
+		}
+
+		FOFsector = true;
+
+		TAG_ITER_SECTORS(sec->lines[i]->args[0], targetsecnum)
+		{
+			sector_t *targetsec = &sectors[targetsecnum];
+			count += ACS_SectorThingCounter(targetsec, thingTag, filter);
+		}
+	}
+
+	if (FOFsector == false)
+	{
+		count += ACS_SectorThingCounter(sec, thingTag, filter);
+	}
+
+	return count;
+}
+
+static UINT32 ACS_SectorTagThingCounter(mtag_t sectorTag, sector_t *activator, mtag_t thingTag, bool (*filter)(mobj_t *))
+{
+	UINT32 count = 0;
+
+	if (sectorTag == 0)
+	{
+		count += ACS_SectorIterateThingCounter(activator, thingTag, filter);
+	}
+	else
+	{
+		INT32 secnum = -1;
+
+		TAG_ITER_SECTORS(sectorTag, secnum)
+		{
+			sector_t *sec = &sectors[secnum];
+			count += ACS_SectorIterateThingCounter(sec, thingTag, filter);
+		}
+	}
+
+	return count;
+}
+
+/*--------------------------------------------------
+	bool CallFunc_Random(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+
+		ACS wrapper for P_RandomRange.
+--------------------------------------------------*/
+bool CallFunc_Random(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+{
+	INT32 low = 0;
+	INT32 high = 0;
+
+	(void)argC;
+
+	low = argV[0];
+	high = argV[1];
+
+	thread->dataStk.push(P_RandomRange(low, high));
+	return false;
+}
+
+/*--------------------------------------------------
+	bool CallFunc_ThingCount(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+
+		Counts the number of things of a particular
+		type and tid. Both fields are optional;
+		no type means indescriminate against type,
+		no tid means search thru all thinkers.
+--------------------------------------------------*/
+static mobjtype_t filter_for_mobjtype = MT_NULL; // annoying but I don't wanna mess with other code
+bool ACS_ThingTypeFilter(mobj_t *mo)
+{
+	return (ACS_CountThing(mo, filter_for_mobjtype));
+}
+
+bool CallFunc_ThingCount(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+{
+	ACSVM::MapScope *map = NULL;
+	ACSVM::String *str = NULL;
+	const char *className = NULL;
+	size_t classLen = 0;
+
+	mobjtype_t type = MT_NULL;
+	mtag_t tid = 0;
+	mtag_t sectorTag = 0;
+
+	size_t count = 0;
+
+	map = thread->scopeMap;
+	str = map->getString(argV[0]);
+
+	className = str->str;
+	classLen = str->len;
+
+	if (classLen > 0)
+	{
+		bool success = ACS_GetMobjTypeFromString(className, &type);
+
+		if (success == false)
+		{
+			// Exit early.
+
+			CONS_Alert(CONS_WARNING,
+				"Couldn't find object type \"%s\" for ThingCount.\n",
+				className
+			);
+
+			return false;
+		}
+	}
+
+	tid = argV[1];
+
+	if (argC > 2)
+	{
+		sectorTag = argV[2];
+	}
+
+	if (sectorTag != 0)
+	{
+		// Search through sectors.
+		filter_for_mobjtype = type;
+		count = ACS_SectorTagThingCounter(sectorTag, nullptr, tid, ACS_ThingTypeFilter);
+		filter_for_mobjtype = MT_NULL;
+	}
+	else if (tid != 0)
+	{
+		// Search through tag lists.
+		mobj_t *mobj = nullptr;
+
+		while ((mobj = P_FindMobjFromTID(tid, mobj, nullptr)) != nullptr)
+		{
+			if (ACS_CountThing(mobj, type) == true)
+			{
+				++count;
+			}
+		}
+	}
+	else
+	{
+		// Search thinkers instead of tag lists.
+		thinker_t *th = nullptr;
+		mobj_t *mobj = nullptr;
+
+		for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next)
+		{
+			if (th->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
+			{
+				continue;
+			}
+
+			mobj = (mobj_t *)th;
+
+			if (ACS_CountThing(mobj, type) == true)
+			{
+				++count;
+			}
+		}
+	}
+
+	thread->dataStk.push(count);
+	return false;
+}
+
+/*--------------------------------------------------
+	bool CallFunc_TagWait(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+
+		Pauses the thread until the tagged
+		sector stops moving.
+--------------------------------------------------*/
+bool CallFunc_TagWait(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+{
+	(void)argC;
+
+	thread->state = {
+		ACSVM::ThreadState::WaitTag,
+		argV[0],
+		ACS_TAGTYPE_SECTOR
+	};
+
+	return true; // Execution interrupted
+}
+
+/*--------------------------------------------------
+	bool CallFunc_PolyWait(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+
+		Pauses the thread until the tagged
+		polyobject stops moving.
+--------------------------------------------------*/
+bool CallFunc_PolyWait(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+{
+	(void)argC;
+
+	thread->state = {
+		ACSVM::ThreadState::WaitTag,
+		argV[0],
+		ACS_TAGTYPE_POLYOBJ
+	};
+
+	return true; // Execution interrupted
+}
+
+/*--------------------------------------------------
+	bool CallFunc_CameraWait(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+
+		Pauses the thread until the tagged
+		camera is done moving.
+--------------------------------------------------*/
+bool CallFunc_CameraWait(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+{
+	(void)argC;
+
+	thread->state = {
+		ACSVM::ThreadState::WaitTag,
+		argV[0],
+		ACS_TAGTYPE_CAMERA
+	};
+
+	thread->dataStk.push(0);
+
+	return true; // Execution interrupted
+}
+
+/*--------------------------------------------------
+	bool CallFunc_ChangeFloor(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+
+		Changes a floor texture.
+--------------------------------------------------*/
+bool CallFunc_ChangeFloor(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+{
+	ACSVM::MapScope *map = nullptr;
+	ACSVM::String *str = nullptr;
+	const char *texName = nullptr;
+
+	INT32 secnum = -1;
+	mtag_t tag = 0;
+
+	(void)argC;
+
+	tag = argV[0];
+
+	map = thread->scopeMap;
+	str = map->getString(argV[1]);
+	texName = str->str;
+
+	TAG_ITER_SECTORS(tag, secnum)
+	{
+		sector_t *sec = &sectors[secnum];
+		sec->floorpic = P_AddLevelFlatRuntime(texName);
+	}
+
+	return false;
+}
+
+/*--------------------------------------------------
+	bool CallFunc_ChangeCeiling(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+
+		Changes a ceiling texture.
+--------------------------------------------------*/
+bool CallFunc_ChangeCeiling(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+{
+	ACSVM::MapScope *map = NULL;
+	ACSVM::String *str = NULL;
+	const char *texName = NULL;
+
+	INT32 secnum = -1;
+	mtag_t tag = 0;
+
+	(void)argC;
+
+	tag = argV[0];
+
+	map = thread->scopeMap;
+	str = map->getString(argV[1]);
+	texName = str->str;
+
+	TAG_ITER_SECTORS(tag, secnum)
+	{
+		sector_t *sec = &sectors[secnum];
+		sec->ceilingpic = P_AddLevelFlatRuntime(texName);
+	}
+
+	return false;
+}
+
+/*--------------------------------------------------
+	bool CallFunc_LineSide(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+
+		Pushes which side of the linedef was
+		activated.
+--------------------------------------------------*/
+bool CallFunc_LineSide(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+{
+	auto info = &static_cast<Thread *>(thread)->info;
+
+	(void)argV;
+	(void)argC;
+
+	thread->dataStk.push(info->side);
+	return false;
+}
+
+/*--------------------------------------------------
+	bool CallFunc_ClearLineSpecial(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+
+		If there is an activating linedef, set its
+		special to 0.
+--------------------------------------------------*/
+bool CallFunc_ClearLineSpecial(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+{
+	auto info = &static_cast<Thread *>(thread)->info;
+
+	(void)argV;
+	(void)argC;
+
+	if (info->line != NULL)
+	{
+		// One time only.
+		info->line->special = 0;
+	}
+
+	return false;
+}
+
+/*--------------------------------------------------
+	bool CallFunc_EndPrint(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+
+		One of the ACS wrappers for CEcho. This
+		version only prints if the activator is a
+		display player.
+--------------------------------------------------*/
+bool CallFunc_EndPrint(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+{
+	(void)argV;
+	(void)argC;
+
+	auto& info = static_cast<Thread*>(thread)->info;
+
+	if (P_MobjWasRemoved(info.mo) == false && info.mo->player != nullptr)
+	{
+		if (ACS_ActivatorIsLocal(thread))
+			HU_DoCEcho(thread->printBuf.data());
+	}
+
+	thread->printBuf.drop();
+	return false;
+}
+
+/*--------------------------------------------------
+	bool CallFunc_PlayerCount(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+
+		Pushes the number of players to ACS.
+--------------------------------------------------*/
+bool CallFunc_PlayerCount(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+{
+	UINT8 numPlayers = 0;
+	UINT8 i;
+
+	(void)argV;
+	(void)argC;
+
+	for (i = 0; i < MAXPLAYERS; i++)
+	{
+		player_t *player = NULL;
+
+		if (playeringame[i] == false)
+		{
+			continue;
+		}
+
+		player = &players[i];
+
+		if (player->spectator == true)
+		{
+			continue;
+		}
+
+		numPlayers++;
+	}
+
+	thread->dataStk.push(numPlayers);
+	return false;
+}
+
+/*--------------------------------------------------
+	bool CallFunc_GameType(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+
+		Pushes the current gametype to ACS.
+--------------------------------------------------*/
+bool CallFunc_GameType(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+{
+	(void)argV;
+	(void)argC;
+
+	thread->dataStk.push(gametype);
+	return false;
+}
+
+/*--------------------------------------------------
+	bool CallFunc_Timer(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+
+		Pushes leveltime to ACS.
+--------------------------------------------------*/
+bool CallFunc_Timer(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+{
+	(void)argV;
+	(void)argC;
+
+	thread->dataStk.push(leveltime);
+	return false;
+}
+
+/*--------------------------------------------------
+	bool CallFunc_IsNetworkGame(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+
+		Pushes netgame status to ACS.
+--------------------------------------------------*/
+bool CallFunc_IsNetworkGame(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+{
+	(void)argV;
+	(void)argC;
+
+	thread->dataStk.push(netgame);
+	return false;
+}
+
+/*--------------------------------------------------
+	bool CallFunc_SectorSound(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+
+		Plays a point sound effect from a sector.
+--------------------------------------------------*/
+bool CallFunc_SectorSound(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+{
+	auto info = &static_cast<Thread *>(thread)->info;
+
+	ACSVM::MapScope *map = nullptr;
+	ACSVM::String *str = nullptr;
+
+	const char *sfxName = nullptr;
+	size_t sfxLen = 0;
+
+	sfxenum_t sfxId = sfx_None;
+	INT32 vol = 0;
+	mobj_t *origin = nullptr;
+
+	(void)argC;
+
+	map = thread->scopeMap;
+	str = map->getString(argV[0]);
+
+	sfxName = str->str;
+	sfxLen = str->len;
+
+	if (sfxLen > 0)
+	{
+		bool success = ACS_GetSFXFromString(sfxName, &sfxId);
+
+		if (success == false)
+		{
+			// Exit early.
+
+			CONS_Alert(CONS_WARNING,
+				"Couldn't find sfx named \"%s\" for SectorSound.\n",
+				sfxName
+			);
+
+			return false;
+		}
+	}
+
+	vol = argV[1];
+
+	if (info->sector != nullptr)
+	{
+		// New to Ring Racers: Use activating sector directly.
+		origin = static_cast<mobj_t *>(static_cast<void *>(&info->sector->soundorg));
+	}
+	else if (info->line != nullptr)
+	{
+		// Original Hexen behavior: Use line's frontsector.
+		origin = static_cast<mobj_t *>(static_cast<void *>(&info->line->frontsector->soundorg));
+	}
+
+	S_StartSoundAtVolume(origin, sfxId, vol);
+	return false;
+}
+
+/*--------------------------------------------------
+	bool CallFunc_AmbientSound(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+
+		Plays a sound effect globally.
+--------------------------------------------------*/
+bool CallFunc_AmbientSound(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+{
+	ACSVM::MapScope *map = nullptr;
+	ACSVM::String *str = nullptr;
+
+	const char *sfxName = nullptr;
+	size_t sfxLen = 0;
+
+	sfxenum_t sfxId = sfx_None;
+	INT32 vol = 0;
+
+	(void)argC;
+
+	map = thread->scopeMap;
+	str = map->getString(argV[0]);
+
+	sfxName = str->str;
+	sfxLen = str->len;
+
+	if (sfxLen > 0)
+	{
+		bool success = ACS_GetSFXFromString(sfxName, &sfxId);
+
+		if (success == false)
+		{
+			// Exit early.
+
+			CONS_Alert(CONS_WARNING,
+				"Couldn't find sfx named \"%s\" for AmbientSound.\n",
+				sfxName
+			);
+
+			return false;
+		}
+	}
+
+	vol = argV[1];
+
+	S_StartSoundAtVolume(NULL, sfxId, vol);
+	return false;
+}
+
+/*--------------------------------------------------
+	bool CallFunc_SetLineTexture(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+
+		Plays a sound effect globally.
+--------------------------------------------------*/
+enum
+{
+	SLT_POS_TOP,
+	SLT_POS_MIDDLE,
+	SLT_POS_BOTTOM
+};
+
+bool CallFunc_SetLineTexture(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+{
+	mtag_t tag = 0;
+	UINT8 sideId = 0;
+	UINT8 texPos = 0;
+
+	ACSVM::MapScope *map = NULL;
+	ACSVM::String *str = NULL;
+	const char *texName = NULL;
+	INT32 texId = LUMPERROR;
+
+	INT32 lineId = -1;
+
+	(void)argC;
+
+	tag = argV[0];
+	sideId = (argV[1] & 1);
+	texPos = argV[2];
+
+	map = thread->scopeMap;
+	str = map->getString(argV[3]);
+	texName = str->str;
+
+	texId = R_TextureNumForName(texName);
+
+	TAG_ITER_LINES(tag, lineId)
+	{
+		line_t *line = &lines[lineId];
+		side_t *side = NULL;
+
+		if (line->sidenum[sideId] != 0xffff)
+		{
+			side = &sides[line->sidenum[sideId]];
+		}
+
+		if (side == NULL)
+		{
+			continue;
+		}
+
+		switch (texPos)
+		{
+			case SLT_POS_MIDDLE:
+			{
+				side->midtexture = texId;
+				break;
+			}
+			case SLT_POS_BOTTOM:
+			{
+				side->bottomtexture = texId;
+				break;
+			}
+			case SLT_POS_TOP:
+			default:
+			{
+				side->toptexture = texId;
+				break;
+			}
+		}
+	}
+
+	return false;
+}
+
+/*--------------------------------------------------
+	bool CallFunc_SetLineSpecial(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+
+		Changes a linedef's special and arguments.
+--------------------------------------------------*/
+bool CallFunc_SetLineSpecial(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+{
+	auto info = &static_cast<Thread *>(thread)->info;
+
+	mtag_t tag = 0;
+	INT32 spec = 0;
+	size_t numArgs = 0;
+
+	INT32 lineId = -1;
+
+	tag = argV[0];
+	spec = argV[1];
+
+	numArgs = std::min(std::max((signed)(argC - 2), 0), NUM_SCRIPT_ARGS);
+
+	TAG_ITER_LINES(tag, lineId)
+	{
+		line_t *line = &lines[lineId];
+		size_t i;
+
+		if (info->line != nullptr && line == info->line)
+		{
+			continue;
+		}
+
+		line->special = spec;
+
+		for (i = 0; i < numArgs; i++)
+		{
+			line->args[i] = argV[i + 2];
+		}
+	}
+
+	return false;
+}
+
+/*--------------------------------------------------
+	bool CallFunc_ChangeSky(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+
+		Changes the map's sky texture.
+--------------------------------------------------*/
+bool CallFunc_ChangeSky(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+{
+	(void)thread;
+	(void)argC;
+
+	P_SetupLevelSky(argV[0], argV[1]);
+
+	return false;
+}
+
+/*--------------------------------------------------
+	bool CallFunc_ThingSound(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+
+		Plays a sound effect for a tagged object.
+--------------------------------------------------*/
+bool CallFunc_ThingSound(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+{
+	auto info = &static_cast<Thread *>(thread)->info;
+
+	ACSVM::MapScope *map = nullptr;
+	ACSVM::String *str = nullptr;
+
+	const char *sfxName = nullptr;
+	size_t sfxLen = 0;
+
+	mtag_t tag = 0;
+	sfxenum_t sfxId = sfx_None;
+	INT32 vol = 0;
+
+	mobj_t *mobj = nullptr;
+
+	(void)argC;
+
+	tag = argV[0];
+
+	map = thread->scopeMap;
+	str = map->getString(argV[1]);
+
+	sfxName = str->str;
+	sfxLen = str->len;
+
+	if (sfxLen > 0)
+	{
+		bool success = ACS_GetSFXFromString(sfxName, &sfxId);
+
+		if (success == false)
+		{
+			// Exit early.
+
+			CONS_Alert(CONS_WARNING,
+				"Couldn't find sfx named \"%s\" for AmbientSound.\n",
+				sfxName
+			);
+
+			return false;
+		}
+	}
+
+	vol = argV[2];
+
+	while ((mobj = P_FindMobjFromTID(tag, mobj, info->mo)) != nullptr)
+	{
+		S_StartSoundAtVolume(mobj, sfxId, vol);
+	}
+
+	return false;
+}
+
+
+/*--------------------------------------------------
+	bool CallFunc_EndPrintBold(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+
+		One of the ACS wrappers for CEcho. This
+		version prints for all players.
+--------------------------------------------------*/
+bool CallFunc_EndPrintBold(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+{
+	(void)argV;
+	(void)argC;
+
+	HU_DoCEcho(thread->printBuf.data());
+
+	thread->printBuf.drop();
+	return false;
+}
+/*--------------------------------------------------
+	bool CallFunc_PlayerTeam(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+
+		Returns the activating player's team ID.
+--------------------------------------------------*/
+bool CallFunc_PlayerTeam(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+{
+	auto info = &static_cast<Thread *>(thread)->info;
+	UINT8 teamID = 0;
+
+	(void)argV;
+	(void)argC;
+
+	if ((info != NULL)
+		&& (info->mo != NULL && P_MobjWasRemoved(info->mo) == false)
+		&& (info->mo->player != NULL))
+	{
+		teamID = info->mo->player->ctfteam;
+	}
+
+	thread->dataStk.push(teamID);
+	return false;
+}
+
+/*--------------------------------------------------
+	bool CallFunc_PlayerRings(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+
+		Returns the activating player's ring count.
+--------------------------------------------------*/
+bool CallFunc_PlayerRings(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+{
+	auto info = &static_cast<Thread *>(thread)->info;
+	SINT8 rings = 0;
+
+	(void)argV;
+	(void)argC;
+
+	if ((info != NULL)
+		&& (info->mo != NULL && P_MobjWasRemoved(info->mo) == false)
+		&& (info->mo->player != NULL))
+	{
+		rings = info->mo->player->rings;
+	}
+
+	thread->dataStk.push(rings);
+	return false;
+}
+
+/*--------------------------------------------------
+	bool CallFunc_PlayerScore(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+
+		Returns the activating player's score.
+--------------------------------------------------*/
+bool CallFunc_PlayerScore(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+{
+	auto info = &static_cast<Thread *>(thread)->info;
+	UINT32 score = 0;
+
+	(void)argV;
+	(void)argC;
+
+	if ((info != NULL)
+		&& (info->mo != NULL && P_MobjWasRemoved(info->mo) == false)
+		&& (info->mo->player != NULL))
+	{
+		score = info->mo->player->score;
+	}
+
+	thread->dataStk.push(score);
+	return false;
+}
+
+/*--------------------------------------------------
+	bool CallFunc_PlayerNumber(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+
+		Returns the activating player's ID.
+--------------------------------------------------*/
+bool CallFunc_PlayerNumber(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+{
+	auto info = &static_cast<Thread *>(thread)->info;
+	INT16 playerID = -1;
+
+	(void)argV;
+	(void)argC;
+
+	if ((info != NULL)
+		&& (info->mo != NULL && P_MobjWasRemoved(info->mo) == false)
+		&& (info->mo->player != NULL))
+	{
+		playerID = (info->mo->player - players);
+	}
+
+	thread->dataStk.push(playerID);
+	return false;
+}
+
+/*--------------------------------------------------
+	bool CallFunc_ActivatorTID(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+
+		Returns the activating object's TID.
+--------------------------------------------------*/
+bool CallFunc_ActivatorTID(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+{
+	auto info = &static_cast<Thread *>(thread)->info;
+	INT16 tid = 0;
+
+	(void)argV;
+	(void)argC;
+
+	if ((info != NULL)
+		&& (info->mo != NULL && P_MobjWasRemoved(info->mo) == false))
+	{
+		tid = info->mo->tid;
+	}
+
+	thread->dataStk.push(tid);
+	return false;
+}
+
+/*--------------------------------------------------
+	bool CallFunc_EndLog(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+
+		One of the ACS wrappers for CONS_Printf.
+		This version only prints if the activator
+		is a display player.
+--------------------------------------------------*/
+bool CallFunc_EndLog(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+{
+	(void)argV;
+	(void)argC;
+
+	if (ACS_ActivatorIsLocal(thread))
+		CONS_Printf("%s\n", thread->printBuf.data());
+
+	thread->printBuf.drop();
+	return false;
+}
+
+/*--------------------------------------------------
+	bool CallFunc_strcmp(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+
+		ACS wrapper for strcmp.
+--------------------------------------------------*/
+static int ACS_strcmp(ACSVM::String *a, ACSVM::String *b)
+{
+	for (char const *sA = a->str, *sB = b->str; ; ++sA, ++sB)
+	{
+		char cA = *sA, cB = *sB;
+
+		if (cA != cB)
+		{
+			return (cA < cB) ? -1 : 1;
+		}
+
+		if (!cA)
+		{
+			return 0;
+		}
+	}
+}
+
+bool CallFunc_strcmp(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+{
+	ACSVM::MapScope *map = NULL;
+
+	ACSVM::String *strA = nullptr;
+	ACSVM::String *strB = nullptr;
+
+	(void)argC;
+
+	map = thread->scopeMap;
+
+	strA = map->getString(argV[0]);
+	strB = map->getString(argV[1]);
+
+	thread->dataStk.push(ACS_strcmp(strA, strB));
+	return false;
+}
+
+/*--------------------------------------------------
+	bool CallFunc_strcasecmp(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+
+		ACS wrapper for strcasecmp / stricmp.
+--------------------------------------------------*/
+static int ACS_strcasecmp(ACSVM::String *a, ACSVM::String *b)
+{
+	for (char const *sA = a->str, *sB = b->str; ; ++sA, ++sB)
+	{
+		char cA = std::tolower(*sA), cB = std::tolower(*sB);
+
+		if (cA != cB)
+		{
+			return (cA < cB) ? -1 : 1;
+		}
+
+		if (!cA)
+		{
+			return 0;
+		}
+	}
+}
+
+bool CallFunc_strcasecmp(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+{
+	ACSVM::MapScope *map = NULL;
+
+	ACSVM::String *strA = nullptr;
+	ACSVM::String *strB = nullptr;
+
+	(void)argC;
+
+	map = thread->scopeMap;
+
+	strA = map->getString(argV[0]);
+	strB = map->getString(argV[1]);
+
+	thread->dataStk.push(ACS_strcasecmp(strA, strB));
+	return false;
+}
+
+/*--------------------------------------------------
+	bool CallFunc_CountEnemies(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+
+		Returns the number of enemies in the tagged sectors.
+--------------------------------------------------*/
+bool ACS_EnemyFilter(mobj_t *mo)
+{
+	return ((mo->flags & (MF_ENEMY|MF_BOSS)) && mo->health > 0);
+}
+
+bool CallFunc_CountEnemies(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+{
+	auto info = &static_cast<Thread *>(thread)->info;
+
+	mtag_t tag = 0;
+	mtag_t tid = 0;
+	UINT32 count = 0;
+
+	(void)argC;
+
+	tag = argV[0];
+	tid = argV[1];
+	count = ACS_SectorTagThingCounter(tag, info->sector, tid, ACS_EnemyFilter);
+
+	thread->dataStk.push(count);
+	return false;
+}
+
+/*--------------------------------------------------
+	bool CallFunc_CountPushables(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+
+		Returns the number of pushables in the tagged sectors.
+--------------------------------------------------*/
+bool ACS_PushableFilter(mobj_t *mo)
+{
+	return ((mo->flags & MF_PUSHABLE)
+		|| ((mo->info->flags & MF_PUSHABLE) && mo->fuse));
+}
+
+bool CallFunc_CountPushables(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+{
+	auto info = &static_cast<Thread *>(thread)->info;
+
+	mtag_t tag = 0;
+	mtag_t tid = 0;
+	UINT32 count = 0;
+
+	(void)argC;
+
+	tag = argV[0];
+	tid = argV[1];
+	count = ACS_SectorTagThingCounter(tag, info->sector, tid, ACS_PushableFilter);
+
+	thread->dataStk.push(count);
+	return false;
+}
+
+/*--------------------------------------------------
+	bool CallFunc_HaveUnlockable(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+
+		Returns if an unlockable has been gotten.
+--------------------------------------------------*/
+bool CallFunc_HaveUnlockable(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+{
+	UINT32 id = 0;
+	bool unlocked = false;
+
+	(void)argC;
+
+	id = argV[0];
+
+	if (id >= MAXUNLOCKABLES)
+	{
+		CONS_Printf("Bad unlockable ID %d\n", id);
+	}
+	else
+	{
+		unlocked = M_CheckNetUnlockByID(id);
+	}
+
+	thread->dataStk.push(unlocked);
+	return false;
+}
+
+/*--------------------------------------------------
+	bool CallFunc_PlayerSkin(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+
+		Returns the activating player's skin name.
+--------------------------------------------------*/
+bool CallFunc_PlayerSkin(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+{
+	Environment *env = &ACSEnv;
+	auto info = &static_cast<Thread *>(thread)->info;
+
+	(void)argV;
+	(void)argC;
+
+	if ((info != NULL)
+		&& (info->mo != NULL && P_MobjWasRemoved(info->mo) == false)
+		&& (info->mo->player != NULL))
+	{
+		UINT8 skin = info->mo->player->skin;
+		thread->dataStk.push(~env->getString( skins[skin]->name )->idx);
+		return false;
+	}
+
+	thread->dataStk.push(0);
+	return false;
+}
+
+/*--------------------------------------------------
+	bool CallFunc_PlayerBot(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+
+		Returns the activating player's bot status.
+--------------------------------------------------*/
+bool CallFunc_PlayerBot(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+{
+	auto info = &static_cast<Thread *>(thread)->info;
+
+	(void)argV;
+	(void)argC;
+
+	if ((info != NULL)
+		&& (info->mo != NULL && P_MobjWasRemoved(info->mo) == false)
+		&& (info->mo->player != NULL))
+	{
+
+		thread->dataStk.push(info->mo->player->bot);
+		return false;
+	}
+
+	thread->dataStk.push(false);
+	return false;
+}
+
+/*--------------------------------------------------
+	bool CallFunc_PlayerExiting(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+
+		Returns the activating player's exiting status.
+--------------------------------------------------*/
+bool CallFunc_PlayerExiting(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+{
+	auto info = &static_cast<Thread *>(thread)->info;
+
+	(void)argV;
+	(void)argC;
+
+	if ((info != NULL)
+		&& (info->mo != NULL && P_MobjWasRemoved(info->mo) == false)
+		&& (info->mo->player != NULL))
+	{
+		thread->dataStk.push((info->mo->player->exiting != 0));
+		return false;
+	}
+
+	thread->dataStk.push(false);
+	return false;
+}
+
+/*--------------------------------------------------
+	bool CallFunc_SetObjectDye(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+
+		Dyes the activating object.
+--------------------------------------------------*/
+bool CallFunc_SetObjectDye(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+{
+	auto info = &static_cast<Thread *>(thread)->info;
+
+	(void)argC;
+
+	if ((info != NULL)
+		&& (info->mo != NULL && P_MobjWasRemoved(info->mo) == false))
+	{
+		var1 = 0;
+		var2 = argV[0];
+		A_Dye(info->mo);
+	}
+
+	thread->dataStk.push(0);
+
+	return false;
+}
+
+/*--------------------------------------------------
+	bool CallFunc_GetObjectDye(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+
+		Returns the activating object's current dye.
+--------------------------------------------------*/
+bool CallFunc_GetObjectDye(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+{
+	Environment *env = &ACSEnv;
+	auto info = &static_cast<Thread *>(thread)->info;
+	UINT16 dye = SKINCOLOR_NONE;
+
+	(void)argV;
+	(void)argC;
+
+	if ((info != NULL)
+		&& (info->mo != NULL && P_MobjWasRemoved(info->mo) == false))
+	{
+		dye = (info->mo->player != NULL) ? info->mo->player->powers[pw_dye] : info->mo->color;
+	}
+
+	thread->dataStk.push(~env->getString( skincolors[dye].name )->idx);
+	return false;
+}
+
+/*--------------------------------------------------
+	bool CallFunc_PlayerEmeralds(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+
+		Returns the activating player's number of Chaos Emeralds.
+--------------------------------------------------*/
+bool CallFunc_PlayerEmeralds(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+{
+	auto info = &static_cast<Thread *>(thread)->info;
+	UINT8 count = 0;
+
+	(void)argV;
+	(void)argC;
+
+	if ((info != NULL)
+		&& (info->mo != NULL && P_MobjWasRemoved(info->mo) == false)
+		&& (info->mo->player != NULL))
+	{
+		UINT32 emerbits;
+
+		if (G_PlatformGametype())
+			emerbits = emeralds;
+		else
+			emerbits = info->mo->player->powers[pw_emeralds];
+
+		for (unsigned i = 0; i < 7; i++)
+		{
+			if (emerbits & (1 << i))
+				count++;
+		}
+	}
+
+	thread->dataStk.push(count);
+	return false;
+}
+
+/*--------------------------------------------------
+	bool CallFunc_PlayerLap(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+
+		Returns the activating player's current lap.
+--------------------------------------------------*/
+bool CallFunc_PlayerLap(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+{
+	auto info = &static_cast<Thread *>(thread)->info;
+	UINT8 laps = 0;
+
+	(void)argV;
+	(void)argC;
+
+	if ((info != NULL)
+		&& (info->mo != NULL && P_MobjWasRemoved(info->mo) == false)
+		&& (info->mo->player != NULL))
+	{
+		laps = info->mo->player->laps;
+	}
+
+	thread->dataStk.push(laps);
+	return false;
+}
+
+/*--------------------------------------------------
+	bool CallFunc_LowestLap(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+
+		Returns the lowest lap of all of the players in-game.
+--------------------------------------------------*/
+bool CallFunc_LowestLap(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+{
+	(void)argV;
+	(void)argC;
+
+	thread->dataStk.push(P_FindLowestLap());
+	return false;
+}
+
+/*--------------------------------------------------
+	bool CallFunc_RingslingerMode(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+
+		Returns if the map is currently in a shooting gametype.
+--------------------------------------------------*/
+bool CallFunc_RingSlingerMode(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+{
+	(void)argV;
+	(void)argC;
+
+	thread->dataStk.push(G_RingSlingerGametype());
+	return false;
+}
+
+/*--------------------------------------------------
+	bool CallFunc_CaptureTheFlagMode(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+
+		Returns if the map is currently in a Capture The Flag gametype.
+--------------------------------------------------*/
+bool CallFunc_CaptureTheFlagMode(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+{
+	(void)argV;
+	(void)argC;
+
+	thread->dataStk.push((gametyperules & GTR_TEAMFLAGS) != 0);
+	return false;
+}
+
+/*--------------------------------------------------
+	bool CallFunc_TeamGame(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+
+		Returns if the map is currently in a team gametype.
+--------------------------------------------------*/
+bool CallFunc_TeamGame(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+{
+	(void)argV;
+	(void)argC;
+
+	thread->dataStk.push(G_GametypeHasTeams());
+	return false;
+}
+
+/*--------------------------------------------------
+	bool CallFunc_ModeAttacking(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+
+		Returns if the map is a Mode Attack session.
+--------------------------------------------------*/
+bool CallFunc_ModeAttacking(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+{
+	(void)argV;
+	(void)argC;
+
+	thread->dataStk.push((modeattacking != ATTACKING_NONE));
+	return false;
+}
+
+/*--------------------------------------------------
+	bool CallFunc_RecordAttack(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+
+		Returns if the map is a Record Attack session.
+--------------------------------------------------*/
+bool CallFunc_RecordAttack(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+{
+	(void)argV;
+	(void)argC;
+
+	thread->dataStk.push((modeattacking == ATTACKING_RECORD));
+	return false;
+}
+
+/*--------------------------------------------------
+	bool CallFunc_NiGHTSAttack(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+
+		Returns if the map is a NiGHTS Attack session.
+--------------------------------------------------*/
+bool CallFunc_NiGHTSAttack(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+{
+	(void)argV;
+	(void)argC;
+
+	thread->dataStk.push((modeattacking == ATTACKING_NIGHTS));
+	return false;
+}
+
+/*--------------------------------------------------
+	bool CallFunc_SetLineRenderStyle(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+
+		Changes a linedef's blend mode and alpha.
+--------------------------------------------------*/
+bool CallFunc_SetLineRenderStyle(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+{
+	mtag_t tag = 0;
+	patchalphastyle_t blend = AST_COPY;
+	fixed_t alpha = FRACUNIT;
+
+	INT32 lineId = -1;
+
+	(void)thread;
+	(void)argC;
+
+	tag = argV[0];
+
+	switch (argV[1])
+	{
+		case TMB_TRANSLUCENT:
+		default:
+			blend = AST_COPY;
+			break;
+		case TMB_ADD:
+			blend = AST_ADD;
+			break;
+		case TMB_SUBTRACT:
+			blend = AST_SUBTRACT;
+			break;
+		case TMB_REVERSESUBTRACT:
+			blend = AST_REVERSESUBTRACT;
+			break;
+		case TMB_MODULATE:
+			blend = AST_MODULATE;
+			break;
+	}
+
+	alpha = argV[2];
+	alpha = std::clamp(alpha, 0, FRACUNIT);
+
+	TAG_ITER_LINES(tag, lineId)
+	{
+		line_t *line = &lines[lineId];
+
+		line->blendmode = blend;
+		line->alpha = alpha;
+	}
+
+	thread->dataStk.push(0);
+
+	return false;
+}
+
+/*--------------------------------------------------
+	bool CallFunc_MapWarp(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+
+		Immediately warps to another level.
+--------------------------------------------------*/
+
+bool CallFunc_MapWarp(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+{
+	ACSVM::MapScope *map = NULL;
+
+	ACSVM::String *str = nullptr;
+
+	const char *levelName = NULL;
+	size_t levelLen = 0;
+
+	INT16 nextmap = 0;
+
+	(void)argC;
+
+	map = thread->scopeMap;
+
+	str = map->getString(argV[0]);
+
+	levelName = str->str;
+	levelLen = str->len;
+
+	if (!levelLen || !levelName)
+	{
+		CONS_Alert(CONS_WARNING, "MapWarp level name was not provided.\n");
+	}
+
+	if (levelName[0] == 'M' && levelName[1] == 'A' && levelName[2] == 'P' && levelName[5] == '\0')
+	{
+		nextmap = (INT16)M_MapNumber(levelName[3], levelName[4]);
+	}
+
+	if (nextmap == 0)
+	{
+		CONS_Alert(CONS_WARNING, "MapWarp level %s is not valid or loaded.\n", levelName);
+		return false;
+	}
+
+	nextmapoverride = (nextmap + 1);
+
+	if (argV[1] == 0)
+		skipstats = 1;
+
+	if (server)
+		SendNetXCmd(XD_EXITLEVEL, NULL, 0);
+
+	thread->dataStk.push(0);
+
+	return false;
+}
+
+/*--------------------------------------------------
+	bool CallFunc_AddBot(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+
+		Inserts a bot, if there's room for them.
+--------------------------------------------------*/
+bool CallFunc_AddBot(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+{
+	ACSVM::MapScope *map = thread->scopeMap;
+
+	ACSVM::String *skinStr = nullptr;
+	const char *skinname = NULL;
+
+	ACSVM::String *nameStr = nullptr;
+	const char *botname = NULL;
+
+	UINT16 skincolor = SKINCOLOR_NONE;
+
+	SINT8 bottype = BOT_MPAI;
+
+	// Get name
+	skinStr = map->getString(argV[0]);
+	if (skinStr->len != 0)
+		skinname = skinStr->str;
+
+	// Get skincolor
+	if (argC >= 2)
+		skincolor = std::clamp(static_cast<int>(argV[1]), (int)SKINCOLOR_NONE, (int)(MAXSKINCOLORS - 1));
+
+	// Get type
+	if (argC >= 3)
+	{
+		bottype = static_cast<int>(argV[2]);
+		if (bottype < BOT_NONE || bottype > BOT_MPAI)
+		{
+			bottype = BOT_MPAI;
+		}
+	}
+
+	// Get name
+	if (argC >= 3)
+	{
+		nameStr = map->getString(argV[3]);
+		if (nameStr->len != 0)
+		{
+			botname = nameStr->str;
+		}
+	}
+
+	thread->dataStk.push(B_AddBot(skinname, skincolor, botname, bottype));
+
+	return false;
+}
+
+/*--------------------------------------------------
+	bool CallFunc_ExitLevel(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+
+		Exits the level.
+--------------------------------------------------*/
+bool CallFunc_ExitLevel(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+{
+	(void)thread;
+	(void)argV;
+	(void)argC;
+
+	if (argC >= 1)
+	{
+		skipstats = (argV[0] == 0);
+	}
+
+	if (server)
+		SendNetXCmd(XD_EXITLEVEL, NULL, 0);
+
+	return false;
+}
+
+/*--------------------------------------------------
+	bool CallFunc_MusicPlay(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+
+		Play a tune. If it's already playing, restart from the
+		beginning.
+--------------------------------------------------*/
+bool CallFunc_MusicPlay(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+{
+	ACSVM::MapScope *map = thread->scopeMap;
+
+	// 0: str tune - id for the tune to play
+	// 1: [bool foractivator] - only do this if the activator is a player and is being viewed
+
+	if (argC > 1 && argV[1] && !ACS_ActivatorIsLocal(thread))
+	{
+		return false;
+	}
+
+	S_StopMusic();
+	S_ChangeMusicInternal(map->getString(argV[0])->str, true);
+
+	thread->dataStk.push(0);
+
+	return false;
+}
+
+/*--------------------------------------------------
+	bool CallFunc_MusicStopAll(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+
+		Stop every tune that is currently playing.
+--------------------------------------------------*/
+bool CallFunc_MusicStopAll(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+{
+	// 0: [bool foractivator] - only do this if the activator is a player and is being viewed
+
+	if (argC > 0 && argV[0] && !ACS_ActivatorIsLocal(thread))
+	{
+		return false;
+	}
+
+	S_StopMusic();
+
+	thread->dataStk.push(0);
+
+	return false;
+}
+
+/*--------------------------------------------------
+	bool CallFunc_MusicRestore(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+
+		Restores the map music.
+--------------------------------------------------*/
+bool CallFunc_MusicRestore(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+{
+	(void)argV;
+	(void)argC;
+
+	S_ChangeMusicEx(mapmusname, mapmusflags, true, mapmusposition, 0, 0);
+
+	thread->dataStk.push(0);
+
+	return false;
+}
+
+/*--------------------------------------------------
+	bool CallFunc_MusicDim(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+
+		Fade level music into or out of silence.
+--------------------------------------------------*/
+bool CallFunc_MusicDim(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+{
+	(void)thread;
+	(void)argC;
+
+	// 0: int fade time (ms) - time to fade between full volume and silence
+	UINT32 fade = argV[0];
+
+	S_FadeOutStopMusic(fade);
+
+	thread->dataStk.push(0);
+
+	return false;
+}
+
+/*--------------------------------------------------
+	bool CallFunc_Get/SetLineProperty(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+
+		Generic line property management.
+--------------------------------------------------*/
+enum
+{
+	LINE_PROP_FLAGS,
+	LINE_PROP_ALPHA,
+	LINE_PROP_BLENDMODE,
+	LINE_PROP_ACTIVATION,
+	LINE_PROP_ACTION,
+	LINE_PROP_ARG0,
+	LINE_PROP_ARG1,
+	LINE_PROP_ARG2,
+	LINE_PROP_ARG3,
+	LINE_PROP_ARG4,
+	LINE_PROP_ARG5,
+	LINE_PROP_ARG6,
+	LINE_PROP_ARG7,
+	LINE_PROP_ARG8,
+	LINE_PROP_ARG9,
+	LINE_PROP_ARG0STR,
+	LINE_PROP_ARG1STR,
+	LINE_PROP__MAX
+};
+
+static INT32 NextLine(mtag_t tag, size_t *iterate, INT32 activatorID)
+{
+	size_t i = *iterate;
+	*iterate = *iterate + 1;
+
+	if (tag == 0)
+	{
+		// 0 grabs the activator.
+
+		if (i != 0)
+		{
+			// Don't do more than once.
+			return -1;
+		}
+
+		return activatorID;
+	}
+
+	return Tag_Iterate_Lines(tag, i);
+}
+
+bool CallFunc_GetLineProperty(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+{
+	(void)thread;
+	(void)argC;
+
+	auto info = &static_cast<Thread *>(thread)->info;
+	Environment *env = &ACSEnv;
+
+	mtag_t tag = 0;
+	size_t tagIt = 0;
+
+	INT32 lineID = 0;
+	INT32 activatorID = -1;
+	line_t *line = NULL;
+
+	INT32 property = LINE_PROP__MAX;
+	INT32 value = 0;
+
+	tag = argV[0];
+
+	if (info != NULL && info->line != NULL)
+	{
+		activatorID = info->line - lines;
+	}
+
+	if ((lineID = NextLine(tag, &tagIt, activatorID)) != -1)
+	{
+		line = &lines[ lineID ];
+	}
+
+	property = argV[1];
+
+	if (line != NULL)
+	{
+
+#define PROP_INT(x, y) \
+	case x: \
+	{ \
+		value = static_cast<INT32>( line->y ); \
+		break; \
+	}
+
+#define PROP_STR(x, y) \
+	case x: \
+	{ \
+		value = static_cast<INT32>( ~env->getString( line->y )->idx ); \
+		break; \
+	}
+
+		switch (property)
+		{
+			PROP_INT(LINE_PROP_FLAGS, flags)
+			PROP_INT(LINE_PROP_ALPHA, alpha)
+			PROP_INT(LINE_PROP_BLENDMODE, blendmode)
+			PROP_INT(LINE_PROP_ACTIVATION, activation)
+			PROP_INT(LINE_PROP_ACTION, special)
+			PROP_INT(LINE_PROP_ARG0, args[0])
+			PROP_INT(LINE_PROP_ARG1, args[1])
+			PROP_INT(LINE_PROP_ARG2, args[2])
+			PROP_INT(LINE_PROP_ARG3, args[3])
+			PROP_INT(LINE_PROP_ARG4, args[4])
+			PROP_INT(LINE_PROP_ARG5, args[5])
+			PROP_INT(LINE_PROP_ARG6, args[6])
+			PROP_INT(LINE_PROP_ARG7, args[7])
+			PROP_INT(LINE_PROP_ARG8, args[8])
+			PROP_INT(LINE_PROP_ARG9, args[9])
+			PROP_STR(LINE_PROP_ARG0STR, stringargs[0])
+			PROP_STR(LINE_PROP_ARG1STR, stringargs[1])
+			default:
+			{
+				CONS_Alert(CONS_WARNING, "GetLineProperty type %d out of range (expected 0 - %d).\n", property, LINE_PROP__MAX-1);
+				break;
+			}
+		}
+
+#undef PROP_STR
+#undef PROP_INT
+
+	}
+
+	thread->dataStk.push(value);
+	return false;
+}
+
+bool CallFunc_SetLineProperty(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+{
+	(void)thread;
+	(void)argC;
+
+	auto info = &static_cast<Thread *>(thread)->info;
+	//Environment *env = &ACSEnv;
+
+	mtag_t tag = 0;
+	size_t tagIt = 0;
+
+	INT32 lineID = 0;
+	INT32 activatorID = -1;
+	line_t *line = NULL;
+
+	INT32 property = LINE_PROP__MAX;
+	INT32 value = 0;
+
+	tag = argV[0];
+
+	if (info != NULL && info->line != NULL)
+	{
+		activatorID = info->line - lines;
+	}
+
+	if ((lineID = NextLine(tag, &tagIt, activatorID)) != -1)
+	{
+		line = &lines[ lineID ];
+	}
+
+	property = argV[1];
+	value = argV[2];
+
+	while (line != NULL)
+	{
+
+#define PROP_READONLY(x, y) \
+	case x: \
+	{ \
+		CONS_Alert(CONS_WARNING, "SetLineProperty type '%s' cannot be written to.\n", "y"); \
+		break; \
+	}
+
+#define PROP_INT(x, y) \
+	case x: \
+	{ \
+		line->y = static_cast< decltype(line->y) >(value); \
+		break; \
+	}
+
+#define PROP_STR(x, y) \
+	case x: \
+	{ \
+		ACSVM::String *str = thread->scopeMap->getString( value ); \
+		if (str->len == 0) \
+		{ \
+			Z_Free(line->y); \
+			line->y = NULL; \
+		} \
+		else \
+		{ \
+			line->y = static_cast<char *>(Z_Realloc(line->y, str->len + 1, PU_LEVEL, NULL)); \
+			M_Memcpy(line->y, str->str, str->len + 1); \
+			line->y[str->len] = '\0'; \
+		} \
+		break; \
+	}
+
+		switch (property)
+		{
+			PROP_INT(LINE_PROP_FLAGS, flags)
+			PROP_INT(LINE_PROP_ALPHA, alpha)
+			PROP_INT(LINE_PROP_BLENDMODE, blendmode)
+			PROP_INT(LINE_PROP_ACTIVATION, activation)
+			PROP_INT(LINE_PROP_ACTION, special)
+			PROP_INT(LINE_PROP_ARG0, args[0])
+			PROP_INT(LINE_PROP_ARG1, args[1])
+			PROP_INT(LINE_PROP_ARG2, args[2])
+			PROP_INT(LINE_PROP_ARG3, args[3])
+			PROP_INT(LINE_PROP_ARG4, args[4])
+			PROP_INT(LINE_PROP_ARG5, args[5])
+			PROP_INT(LINE_PROP_ARG6, args[6])
+			PROP_INT(LINE_PROP_ARG7, args[7])
+			PROP_INT(LINE_PROP_ARG8, args[8])
+			PROP_INT(LINE_PROP_ARG9, args[9])
+			PROP_STR(LINE_PROP_ARG0STR, stringargs[0])
+			PROP_STR(LINE_PROP_ARG1STR, stringargs[1])
+			default:
+			{
+				CONS_Alert(CONS_WARNING, "SetLineProperty type %d out of range (expected 0 - %d).\n", property, LINE_PROP__MAX-1);
+				break;
+			}
+		}
+
+		if ((lineID = NextLine(tag, &tagIt, activatorID)) != -1)
+		{
+			line = &lines[ lineID ];
+		}
+		else
+		{
+			line = NULL;
+		}
+
+#undef PROP_STR
+#undef PROP_INT
+#undef PROP_READONLY
+
+	}
+
+	thread->dataStk.push(0);
+
+	return false;
+}
+
+/*--------------------------------------------------
+	bool CallFunc_Get/SetSideProperty(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+
+		Generic side property management.
+--------------------------------------------------*/
+enum
+{
+	SIDE_FRONT = 0,
+	SIDE_BACK = 1,
+	SIDE_BOTH,
+};
+
+enum
+{
+	SIDE_PROP_XOFFSET,
+	SIDE_PROP_YOFFSET,
+	SIDE_PROP_TOPTEXTURE,
+	SIDE_PROP_BOTTOMTEXTURE,
+	SIDE_PROP_MIDTEXTURE,
+	SIDE_PROP_REPEATCOUNT,
+	SIDE_PROP__MAX
+};
+
+bool CallFunc_GetSideProperty(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+{
+	(void)thread;
+	(void)argC;
+
+	auto info = &static_cast<Thread *>(thread)->info;
+	Environment *env = &ACSEnv;
+
+	mtag_t tag = 0;
+	size_t tagIt = 0;
+
+	INT32 lineID = 0;
+	INT32 activatorID = -1;
+	line_t *line = NULL;
+
+	UINT8 sideID = 0;
+	side_t *side = NULL;
+
+	INT32 property = SIDE_PROP__MAX;
+	INT32 value = 0;
+
+	tag = argV[0];
+
+	if (info != NULL && info->line != NULL)
+	{
+		activatorID = info->line - lines;
+	}
+
+	if ((lineID = NextLine(tag, &tagIt, activatorID)) != -1)
+	{
+		line = &lines[ lineID ];
+	}
+
+	sideID = argV[1];
+	switch (sideID)
+	{
+		default: // Activator
+		case SIDE_BOTH: // Wouldn't make sense for this function.
+		{
+			sideID = info->side;
+			break;
+		}
+		case SIDE_FRONT:
+		case SIDE_BACK:
+		{
+			// Keep sideID as is.
+			break;
+		}
+	}
+
+	if (line != NULL && line->sidenum[sideID] != 0xffff)
+	{
+		side = &sides[line->sidenum[sideID]];
+	}
+
+	property = argV[2];
+
+	if (side != NULL)
+	{
+
+#define PROP_INT(x, y) \
+	case x: \
+	{ \
+		value = static_cast<INT32>( side->y ); \
+		break; \
+	}
+
+#define PROP_STR(x, y) \
+	case x: \
+	{ \
+		value = static_cast<INT32>( ~env->getString( side->y )->idx ); \
+		break; \
+	}
+
+#define PROP_TEXTURE(x, y) \
+	case x: \
+	{ \
+		value = static_cast<INT32>( ~env->getString( textures[ side->y ]->name )->idx ); \
+		break; \
+	}
+
+		switch (property)
+		{
+			PROP_INT(SIDE_PROP_XOFFSET, textureoffset)
+			PROP_INT(SIDE_PROP_YOFFSET, rowoffset)
+			PROP_TEXTURE(SIDE_PROP_TOPTEXTURE, toptexture)
+			PROP_TEXTURE(SIDE_PROP_BOTTOMTEXTURE, bottomtexture)
+			PROP_TEXTURE(SIDE_PROP_MIDTEXTURE, midtexture)
+			PROP_INT(SIDE_PROP_REPEATCOUNT, repeatcnt)
+			default:
+			{
+				CONS_Alert(CONS_WARNING, "GetSideProperty type %d out of range (expected 0 - %d).\n", property, SIDE_PROP__MAX-1);
+				break;
+			}
+		}
+
+#undef PROP_TEXTURE
+#undef PROP_STR
+#undef PROP_INT
+
+	}
+
+	thread->dataStk.push(value);
+	return false;
+}
+
+bool CallFunc_SetSideProperty(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+{
+	(void)thread;
+	(void)argC;
+
+	auto info = &static_cast<Thread *>(thread)->info;
+	//Environment *env = &ACSEnv;
+
+	mtag_t tag = 0;
+	size_t tagIt = 0;
+
+	INT32 lineID = 0;
+	INT32 activatorID = -1;
+	line_t *line = NULL;
+
+	UINT8 sideID = 0;
+	side_t *side = NULL;
+	boolean tryBoth = false;
+
+	INT32 property = SIDE_PROP__MAX;
+	INT32 value = 0;
+
+	tag = argV[0];
+
+	if (info != NULL && info->line != NULL)
+	{
+		activatorID = info->line - lines;
+	}
+
+	if ((lineID = NextLine(tag, &tagIt, activatorID)) != -1)
+	{
+		line = &lines[ lineID ];
+	}
+
+	sideID = argV[1];
+	switch (sideID)
+	{
+		default: // Activator
+		{
+			sideID = info->side;
+			break;
+		}
+		case SIDE_BOTH:
+		{
+			sideID = SIDE_FRONT;
+			tryBoth = true;
+			break;
+		}
+		case SIDE_FRONT:
+		case SIDE_BACK:
+		{
+			// Keep sideID as is.
+			break;
+		}
+	}
+
+	if (line != NULL && line->sidenum[sideID] != 0xffff)
+	{
+		side = &sides[line->sidenum[sideID]];
+	}
+
+	property = argV[2];
+	value = argV[3];
+
+	while (line != NULL)
+	{
+		if (side != NULL)
+		{
+
+#define PROP_READONLY(x, y) \
+	case x: \
+	{ \
+		CONS_Alert(CONS_WARNING, "SetSideProperty type '%s' cannot be written to.\n", "y"); \
+		break; \
+	}
+
+#define PROP_INT(x, y) \
+	case x: \
+	{ \
+		side->y = static_cast< decltype(side->y) >(value); \
+		break; \
+	}
+
+#define PROP_STR(x, y) \
+	case x: \
+	{ \
+		ACSVM::String *str = thread->scopeMap->getString( value ); \
+		if (str->len == 0) \
+		{ \
+			Z_Free(side->y); \
+			side->y = NULL; \
+		} \
+		else \
+		{ \
+			side->y = static_cast<char *>(Z_Realloc(side->y, str->len + 1, PU_LEVEL, NULL)); \
+			M_Memcpy(side->y, str->str, str->len + 1); \
+			side->y[str->len] = '\0'; \
+		} \
+		break; \
+	}
+
+#define PROP_TEXTURE(x, y) \
+	case x: \
+	{ \
+		side->y = R_TextureNumForName( thread->scopeMap->getString( value )->str ); \
+		break; \
+	}
+
+			auto install_interpolator = [side]
+			{
+				if (side->acs_interpolated)
+					return;
+				side->acs_interpolated = true;
+				R_CreateInterpolator_SideScroll(nullptr, side);
+			};
+
+			switch (property)
+			{
+				case SIDE_PROP_XOFFSET:
+				{
+					side->textureoffset = static_cast< decltype(side->textureoffset) >(value);
+					install_interpolator();
+					break;
+				}
+				case SIDE_PROP_YOFFSET:
+				{
+					side->rowoffset = static_cast< decltype(side->rowoffset) >(value);
+					install_interpolator();
+					break;
+				}
+				PROP_TEXTURE(SIDE_PROP_TOPTEXTURE, toptexture)
+				PROP_TEXTURE(SIDE_PROP_BOTTOMTEXTURE, bottomtexture)
+				PROP_TEXTURE(SIDE_PROP_MIDTEXTURE, midtexture)
+				PROP_INT(SIDE_PROP_REPEATCOUNT, repeatcnt)
+				default:
+				{
+					CONS_Alert(CONS_WARNING, "SetSideProperty type %d out of range (expected 0 - %d).\n", property, SIDE_PROP__MAX-1);
+					break;
+				}
+			}
+		}
+
+		if (tryBoth == true && sideID == SIDE_FRONT)
+		{
+			sideID = SIDE_BACK;
+
+			if (line->sidenum[sideID] != 0xffff)
+			{
+				side = &sides[line->sidenum[sideID]];
+				continue;
+			}
+		}
+
+		if ((lineID = NextLine(tag, &tagIt, activatorID)) != -1)
+		{
+			line = &lines[ lineID ];
+
+			if (tryBoth == true)
+			{
+				sideID = SIDE_FRONT;
+			}
+		}
+		else
+		{
+			line = NULL;
+		}
+
+		if (line != NULL && line->sidenum[sideID] != 0xffff)
+		{
+			side = &sides[line->sidenum[sideID]];
+		}
+		else
+		{
+			side = NULL;
+		}
+
+#undef PROP_TEXTURE
+#undef PROP_STR
+#undef PROP_INT
+#undef PROP_READONLY
+
+	}
+
+	thread->dataStk.push(0);
+
+	return false;
+}
+
+/*--------------------------------------------------
+	bool CallFunc_Get/SetSectorProperty(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+
+		Generic sector property management.
+--------------------------------------------------*/
+enum
+{
+	SECTOR_PROP_FLOORHEIGHT,
+	SECTOR_PROP_CEILINGHEIGHT,
+	SECTOR_PROP_FLOORPIC,
+	SECTOR_PROP_CEILINGPIC,
+	SECTOR_PROP_LIGHTLEVEL,
+	SECTOR_PROP_FLOORLIGHTLEVEL,
+	SECTOR_PROP_CEILINGLIGHTLEVEL,
+	SECTOR_PROP_FLOORLIGHTABSOLUTE,
+	SECTOR_PROP_CEILINGLIGHTABSOLUTE,
+	SECTOR_PROP_FLAGS,
+	SECTOR_PROP_SPECIALFLAGS,
+	SECTOR_PROP_GRAVITY,
+	SECTOR_PROP_ACTIVATION,
+	SECTOR_PROP_ACTION,
+	SECTOR_PROP_ARG0,
+	SECTOR_PROP_ARG1,
+	SECTOR_PROP_ARG2,
+	SECTOR_PROP_ARG3,
+	SECTOR_PROP_ARG4,
+	SECTOR_PROP_ARG5,
+	SECTOR_PROP_ARG6,
+	SECTOR_PROP_ARG7,
+	SECTOR_PROP_ARG8,
+	SECTOR_PROP_ARG9,
+	SECTOR_PROP_ARG0STR,
+	SECTOR_PROP_ARG1STR,
+	SECTOR_PROP__MAX
+};
+
+static INT32 NextSector(mtag_t tag, size_t *iterate, INT32 activatorID)
+{
+	size_t i = *iterate;
+	*iterate = *iterate + 1;
+
+	if (tag == 0)
+	{
+		// 0 grabs the activator.
+
+		if (i != 0)
+		{
+			// Don't do more than once.
+			return -1;
+		}
+
+		return activatorID;
+	}
+
+	return Tag_Iterate_Sectors(tag, i);
+}
+
+bool CallFunc_GetSectorProperty(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+{
+	(void)thread;
+	(void)argC;
+
+	auto info = &static_cast<Thread *>(thread)->info;
+	Environment *env = &ACSEnv;
+
+	mtag_t tag = 0;
+	size_t tagIt = 0;
+
+	INT32 sectorID = 0;
+	INT32 activatorID = -1;
+	sector_t *sector = NULL;
+
+	INT32 property = SECTOR_PROP__MAX;
+	INT32 value = 0;
+
+	tag = argV[0];
+
+	if (info != NULL && info->sector != NULL)
+	{
+		activatorID = info->sector - sectors;
+	}
+
+	if ((sectorID = NextSector(tag, &tagIt, activatorID)) != -1)
+	{
+		sector = &sectors[ sectorID ];
+	}
+
+	property = argV[1];
+
+	if (sector != NULL)
+	{
+
+#define PROP_INT(x, y) \
+	case x: \
+	{ \
+		value = static_cast<INT32>( sector->y ); \
+		break; \
+	}
+
+#define PROP_STR(x, y) \
+	case x: \
+	{ \
+		value = static_cast<INT32>( ~env->getString( sector->y )->idx ); \
+		break; \
+	}
+
+#define PROP_FLAT(x, y) \
+	case x: \
+	{ \
+		value = static_cast<INT32>( ~env->getString( levelflats[ sector->y ].name )->idx ); \
+		break; \
+	}
+
+		switch (property)
+		{
+			PROP_INT(SECTOR_PROP_FLOORHEIGHT, floorheight)
+			PROP_INT(SECTOR_PROP_CEILINGHEIGHT, ceilingheight)
+			PROP_FLAT(SECTOR_PROP_FLOORPIC, floorpic)
+			PROP_FLAT(SECTOR_PROP_CEILINGPIC, ceilingpic)
+			PROP_INT(SECTOR_PROP_LIGHTLEVEL, lightlevel)
+			PROP_INT(SECTOR_PROP_FLOORLIGHTLEVEL, floorlightlevel)
+			PROP_INT(SECTOR_PROP_CEILINGLIGHTLEVEL, ceilinglightlevel)
+			PROP_INT(SECTOR_PROP_FLOORLIGHTABSOLUTE, floorlightabsolute)
+			PROP_INT(SECTOR_PROP_CEILINGLIGHTABSOLUTE, ceilinglightabsolute)
+			PROP_INT(SECTOR_PROP_FLAGS, flags)
+			PROP_INT(SECTOR_PROP_SPECIALFLAGS, specialflags)
+			PROP_INT(SECTOR_PROP_GRAVITY, gravity)
+			PROP_INT(SECTOR_PROP_ACTIVATION, activation)
+			PROP_INT(SECTOR_PROP_ACTION, action)
+			PROP_INT(SECTOR_PROP_ARG0, args[0])
+			PROP_INT(SECTOR_PROP_ARG1, args[1])
+			PROP_INT(SECTOR_PROP_ARG2, args[2])
+			PROP_INT(SECTOR_PROP_ARG3, args[3])
+			PROP_INT(SECTOR_PROP_ARG4, args[4])
+			PROP_INT(SECTOR_PROP_ARG5, args[5])
+			PROP_INT(SECTOR_PROP_ARG6, args[6])
+			PROP_INT(SECTOR_PROP_ARG7, args[7])
+			PROP_INT(SECTOR_PROP_ARG8, args[8])
+			PROP_INT(SECTOR_PROP_ARG9, args[9])
+			PROP_STR(SECTOR_PROP_ARG0STR, stringargs[0])
+			PROP_STR(SECTOR_PROP_ARG1STR, stringargs[1])
+			default:
+			{
+				CONS_Alert(CONS_WARNING, "GetSectorProperty type %d out of range (expected 0 - %d).\n", property, SECTOR_PROP__MAX-1);
+				break;
+			}
+		}
+
+#undef PROP_FLAT
+#undef PROP_STR
+#undef PROP_INT
+
+	}
+
+	thread->dataStk.push(value);
+	return false;
+}
+
+bool CallFunc_SetSectorProperty(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+{
+	(void)thread;
+	(void)argC;
+
+	auto info = &static_cast<Thread *>(thread)->info;
+	//Environment *env = &ACSEnv;
+
+	mtag_t tag = 0;
+	size_t tagIt = 0;
+
+	INT32 sectorID = 0;
+	INT32 activatorID = -1;
+	sector_t *sector = NULL;
+
+	INT32 property = SECTOR_PROP__MAX;
+	INT32 value = 0;
+
+	tag = argV[0];
+
+	if (info != NULL && info->sector != NULL)
+	{
+		activatorID = info->sector - sectors;
+	}
+
+	if ((sectorID = NextSector(tag, &tagIt, activatorID)) != -1)
+	{
+		sector = &sectors[ sectorID ];
+	}
+
+	property = argV[1];
+	value = argV[2];
+
+	while (sector != NULL)
+	{
+
+#define PROP_READONLY(x, y) \
+	case x: \
+	{ \
+		CONS_Alert(CONS_WARNING, "SetSectorProperty type '%s' cannot be written to.\n", "y"); \
+		break; \
+	}
+
+#define PROP_INT(x, y) \
+	case x: \
+	{ \
+		sector->y = static_cast< decltype(sector->y) >(value); \
+		break; \
+	}
+
+#define PROP_STR(x, y) \
+	case x: \
+	{ \
+		ACSVM::String *str = thread->scopeMap->getString( value ); \
+		if (str->len == 0) \
+		{ \
+			Z_Free(sector->y); \
+			sector->y = NULL; \
+		} \
+		else \
+		{ \
+			sector->y = static_cast<char *>(Z_Realloc(sector->y, str->len + 1, PU_LEVEL, NULL)); \
+			M_Memcpy(sector->y, str->str, str->len + 1); \
+			sector->y[str->len] = '\0'; \
+		} \
+		break; \
+	}
+
+#define PROP_FLAT(x, y) \
+	case x: \
+	{ \
+		sector->y = P_AddLevelFlatRuntime( thread->scopeMap->getString( value )->str ); \
+		break; \
+	}
+
+		switch (property)
+		{
+			PROP_INT(SECTOR_PROP_FLOORHEIGHT, floorheight)
+			PROP_INT(SECTOR_PROP_CEILINGHEIGHT, ceilingheight)
+			PROP_FLAT(SECTOR_PROP_FLOORPIC, floorpic)
+			PROP_FLAT(SECTOR_PROP_CEILINGPIC, ceilingpic)
+			PROP_INT(SECTOR_PROP_LIGHTLEVEL, lightlevel)
+			PROP_INT(SECTOR_PROP_FLOORLIGHTLEVEL, floorlightlevel)
+			PROP_INT(SECTOR_PROP_CEILINGLIGHTLEVEL, ceilinglightlevel)
+			PROP_INT(SECTOR_PROP_FLOORLIGHTABSOLUTE, floorlightabsolute)
+			PROP_INT(SECTOR_PROP_CEILINGLIGHTABSOLUTE, ceilinglightabsolute)
+			PROP_INT(SECTOR_PROP_FLAGS, flags)
+			PROP_INT(SECTOR_PROP_SPECIALFLAGS, specialflags)
+			PROP_INT(SECTOR_PROP_GRAVITY, gravity)
+			PROP_INT(SECTOR_PROP_ACTIVATION, activation)
+			PROP_INT(SECTOR_PROP_ACTION, action)
+			PROP_INT(SECTOR_PROP_ARG0, args[0])
+			PROP_INT(SECTOR_PROP_ARG1, args[1])
+			PROP_INT(SECTOR_PROP_ARG2, args[2])
+			PROP_INT(SECTOR_PROP_ARG3, args[3])
+			PROP_INT(SECTOR_PROP_ARG4, args[4])
+			PROP_INT(SECTOR_PROP_ARG5, args[5])
+			PROP_INT(SECTOR_PROP_ARG6, args[6])
+			PROP_INT(SECTOR_PROP_ARG7, args[7])
+			PROP_INT(SECTOR_PROP_ARG8, args[8])
+			PROP_INT(SECTOR_PROP_ARG9, args[9])
+			PROP_STR(SECTOR_PROP_ARG0STR, stringargs[0])
+			PROP_STR(SECTOR_PROP_ARG1STR, stringargs[1])
+			default:
+			{
+				CONS_Alert(CONS_WARNING, "SetSectorProperty type %d out of range (expected 0 - %d).\n", property, SECTOR_PROP__MAX-1);
+				break;
+			}
+		}
+
+		if ((sectorID = NextSector(tag, &tagIt, activatorID)) != -1)
+		{
+			sector = &sectors[ sectorID ];
+		}
+		else
+		{
+			sector = NULL;
+		}
+
+#undef PROP_FLAT
+#undef PROP_STR
+#undef PROP_INT
+#undef PROP_READONLY
+
+	}
+
+	thread->dataStk.push(0);
+
+	return false;
+}
+
+/*--------------------------------------------------
+	bool CallFunc_Get/SetThingProperty(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+
+		Generic thing property management.
+--------------------------------------------------*/
+enum
+{
+	THING_PROP_X,
+	THING_PROP_Y,
+	THING_PROP_Z,
+	THING_PROP_TYPE,
+	THING_PROP_ANGLE,
+	THING_PROP_PITCH,
+	THING_PROP_ROLL,
+	THING_PROP_SPRITEROLL,
+	THING_PROP_FRAME,
+	THING_PROP_SPRITE,
+	THING_PROP_SPRITE2,
+	THING_PROP_RENDERFLAGS,
+	THING_PROP_SPRITEXSCALE,
+	THING_PROP_SPRITEYSCALE,
+	THING_PROP_SPRITEXOFFSET,
+	THING_PROP_SPRITEYOFFSET,
+	THING_PROP_FLOORZ,
+	THING_PROP_CEILINGZ,
+	THING_PROP_RADIUS,
+	THING_PROP_HEIGHT,
+	THING_PROP_MOMX,
+	THING_PROP_MOMY,
+	THING_PROP_MOMZ,
+	THING_PROP_TICS,
+	THING_PROP_STATE,
+	THING_PROP_FLAGS,
+	THING_PROP_FLAGS2,
+	THING_PROP_EFLAGS,
+	THING_PROP_SKIN,
+	THING_PROP_COLOR,
+	THING_PROP_HEALTH,
+	THING_PROP_MOVEDIR,
+	THING_PROP_MOVECOUNT,
+	THING_PROP_REACTIONTIME,
+	THING_PROP_THRESHOLD,
+	THING_PROP_LASTLOOK,
+	THING_PROP_FRICTION,
+	THING_PROP_MOVEFACTOR,
+	THING_PROP_FUSE,
+	THING_PROP_WATERTOP,
+	THING_PROP_WATERBOTTOM,
+	THING_PROP_SCALE,
+	THING_PROP_DESTSCALE,
+	THING_PROP_SCALESPEED,
+	THING_PROP_EXTRAVALUE1,
+	THING_PROP_EXTRAVALUE2,
+	THING_PROP_CUSVAL,
+	THING_PROP_CVMEM,
+	THING_PROP_COLORIZED,
+	THING_PROP_MIRRORED,
+	THING_PROP_SHADOWSCALE,
+	THING_PROP_DISPOFFSET,
+	THING_PROP_TARGET,
+	THING_PROP_TRACER,
+	THING_PROP_HNEXT,
+	THING_PROP_HPREV,
+	THING_PROP__MAX
+};
+
+bool CallFunc_GetThingProperty(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+{
+	(void)thread;
+	(void)argC;
+
+	auto info = &static_cast<Thread *>(thread)->info;
+	Environment *env = &ACSEnv;
+
+	mtag_t tag = 0;
+	mobj_t *mobj = NULL;
+
+	INT32 property = SECTOR_PROP__MAX;
+	INT32 value = 0;
+
+	tag = argV[0];
+	mobj = P_FindMobjFromTID(tag, mobj, info->mo);
+
+	property = argV[1];
+
+	if (mobj != NULL)
+	{
+
+#define PROP_INT(x, y) \
+	case x: \
+	{ \
+		value = static_cast<INT32>( mobj->y ); \
+		break; \
+	}
+
+#define PROP_STR(x, y) \
+	case x: \
+	{ \
+		value = static_cast<INT32>( ~env->getString( mobj->y )->idx ); \
+		break; \
+	}
+
+#define PROP_ANGLE(x, y) \
+	case x: \
+	{ \
+		value = static_cast<INT32>( AngleFixed( mobj->y ) ); \
+		break; \
+	}
+
+#define PROP_TYPE(x, y) \
+	case x: \
+	{ \
+		if (mobj->y >= MT_FIRSTFREESLOT) \
+		{ \
+			std::string	prefix = "MT_"; \
+			std::string	full = prefix + FREE_MOBJS[mobj->y - MT_FIRSTFREESLOT]; \
+			value = static_cast<INT32>( ~env->getString( full.c_str() )->idx ); \
+		} \
+		else \
+		{ \
+			value = static_cast<INT32>( ~env->getString( MOBJTYPE_LIST[ mobj->y ] )->idx ); \
+		} \
+		break; \
+	}
+
+#define PROP_SPR(x, y) \
+	case x: \
+	{ \
+		char crunched[5] = {0}; \
+		strncpy(crunched, sprnames[ mobj->y ], 4); \
+		value = static_cast<INT32>( ~env->getString( crunched )->idx ); \
+		break; \
+	}
+
+#define PROP_SPR2(x, y) \
+	case x: \
+	{ \
+		value = static_cast<INT32>( ~env->getString( spr2names[ mobj->y ] )->idx ); \
+		break; \
+	}
+
+#define PROP_STATE(x, y) \
+	case x: \
+	{ \
+		statenum_t stateID = static_cast<statenum_t>(mobj->y - states); \
+		if (stateID >= S_FIRSTFREESLOT) \
+		{ \
+			std::string	prefix = "S_"; \
+			std::string	full = prefix + FREE_STATES[stateID - S_FIRSTFREESLOT]; \
+			value = static_cast<INT32>( ~env->getString( full.c_str() )->idx ); \
+		} \
+		else \
+		{ \
+			value = static_cast<INT32>( ~env->getString( STATE_LIST[ stateID ] )->idx ); \
+		} \
+		break; \
+	}
+
+#define PROP_SKIN(x, y) \
+	case x: \
+	{ \
+		if (mobj->y != NULL) \
+		{ \
+			skin_t *skin = static_cast<skin_t *>(mobj->y); \
+			value = static_cast<INT32>( ~env->getString( skin->name )->idx ); \
+		} \
+		break; \
+	}
+
+#define PROP_COLOR(x, y) \
+	case x: \
+	{ \
+		value = static_cast<INT32>( ~env->getString( skincolors[ mobj->y ].name )->idx ); \
+		break; \
+	}
+
+#define PROP_MOBJ(x, y) \
+	case x: \
+	{ \
+		if (P_MobjWasRemoved(mobj->y) == false) \
+		{ \
+			value = static_cast<INT32>( mobj->y->tid ); \
+		} \
+		break; \
+	}
+
+		switch (property)
+		{
+			PROP_INT(THING_PROP_X, x)
+			PROP_INT(THING_PROP_Y, y)
+			PROP_INT(THING_PROP_Z, z)
+			PROP_TYPE(THING_PROP_TYPE, type)
+			PROP_ANGLE(THING_PROP_ANGLE, angle)
+			PROP_ANGLE(THING_PROP_PITCH, pitch)
+			PROP_ANGLE(THING_PROP_ROLL, roll)
+			PROP_ANGLE(THING_PROP_SPRITEROLL, spriteroll)
+			PROP_INT(THING_PROP_FRAME, frame)
+			PROP_SPR(THING_PROP_SPRITE, sprite)
+			PROP_SPR2(THING_PROP_SPRITE2, sprite2)
+			PROP_INT(THING_PROP_RENDERFLAGS, renderflags)
+			PROP_INT(THING_PROP_SPRITEXSCALE, spritexscale)
+			PROP_INT(THING_PROP_SPRITEYSCALE, spriteyscale)
+			PROP_INT(THING_PROP_SPRITEXOFFSET, spritexoffset)
+			PROP_INT(THING_PROP_SPRITEYOFFSET, spriteyoffset)
+			PROP_INT(THING_PROP_FLOORZ, floorz)
+			PROP_INT(THING_PROP_CEILINGZ, ceilingz)
+			PROP_INT(THING_PROP_RADIUS, radius)
+			PROP_INT(THING_PROP_HEIGHT, height)
+			PROP_INT(THING_PROP_MOMX, momx)
+			PROP_INT(THING_PROP_MOMY, momy)
+			PROP_INT(THING_PROP_MOMZ, momz)
+			PROP_INT(THING_PROP_TICS, tics)
+			PROP_STATE(THING_PROP_STATE, state)
+			PROP_INT(THING_PROP_FLAGS, flags)
+			PROP_INT(THING_PROP_FLAGS2, flags2)
+			PROP_INT(THING_PROP_EFLAGS, eflags)
+			PROP_SKIN(THING_PROP_SKIN, skin)
+			PROP_COLOR(THING_PROP_COLOR, color)
+			PROP_INT(THING_PROP_HEALTH, health)
+			PROP_INT(THING_PROP_MOVEDIR, movedir)
+			PROP_INT(THING_PROP_MOVECOUNT, movecount)
+			PROP_INT(THING_PROP_REACTIONTIME, reactiontime)
+			PROP_INT(THING_PROP_THRESHOLD, threshold)
+			PROP_INT(THING_PROP_LASTLOOK, lastlook)
+			PROP_INT(THING_PROP_FRICTION, friction)
+			PROP_INT(THING_PROP_MOVEFACTOR, movefactor)
+			PROP_INT(THING_PROP_FUSE, fuse)
+			PROP_INT(THING_PROP_WATERTOP, watertop)
+			PROP_INT(THING_PROP_WATERBOTTOM, waterbottom)
+			PROP_INT(THING_PROP_SCALE, scale)
+			PROP_INT(THING_PROP_DESTSCALE, destscale)
+			PROP_INT(THING_PROP_SCALESPEED, scalespeed)
+			PROP_INT(THING_PROP_EXTRAVALUE1, extravalue1)
+			PROP_INT(THING_PROP_EXTRAVALUE2, extravalue2)
+			PROP_INT(THING_PROP_CUSVAL, cusval)
+			PROP_INT(THING_PROP_CVMEM, cvmem)
+			PROP_INT(THING_PROP_COLORIZED, colorized)
+			PROP_INT(THING_PROP_MIRRORED, mirrored)
+			PROP_INT(THING_PROP_SHADOWSCALE, shadowscale)
+			PROP_INT(THING_PROP_DISPOFFSET, dispoffset)
+			PROP_MOBJ(THING_PROP_TARGET, target)
+			PROP_MOBJ(THING_PROP_TRACER, tracer)
+			PROP_MOBJ(THING_PROP_HNEXT, hnext)
+			PROP_MOBJ(THING_PROP_HPREV, hprev)
+			default:
+			{
+				CONS_Alert(CONS_WARNING, "GetThingProperty type %d out of range (expected 0 - %d).\n", property, THING_PROP__MAX-1);
+				break;
+			}
+		}
+
+#undef PROP_MOBJ
+#undef PROP_COLOR
+#undef PROP_SKIN
+#undef PROP_STATE
+#undef PROP_SPR2
+#undef PROP_SPR
+#undef PROP_TYPE
+#undef PROP_ANGLE
+#undef PROP_STR
+#undef PROP_INT
+
+	}
+
+	thread->dataStk.push(value);
+	return false;
+}
+
+bool CallFunc_SetThingProperty(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+{
+	(void)thread;
+	(void)argC;
+
+	auto info = &static_cast<Thread *>(thread)->info;
+	//Environment *env = &ACSEnv;
+
+	mtag_t tag = 0;
+	mobj_t *mobj = NULL;
+
+	INT32 property = SECTOR_PROP__MAX;
+	INT32 value = 0;
+
+	tag = argV[0];
+	mobj = P_FindMobjFromTID(tag, mobj, info->mo);
+
+	property = argV[1];
+	value = argV[2];
+
+	while (mobj != NULL)
+	{
+
+#define PROP_READONLY(x, y) \
+	case x: \
+	{ \
+		CONS_Alert(CONS_WARNING, "SetThingProperty type '%s' cannot be written to.\n", "y"); \
+		break; \
+	}
+
+#define PROP_INT(x, y) \
+	case x: \
+	{ \
+		mobj->y = static_cast< decltype(mobj->y) >(value); \
+		break; \
+	}
+
+#define PROP_STR(x, y) \
+	case x: \
+	{ \
+		ACSVM::String *str = thread->scopeMap->getString( value ); \
+		if (str->len == 0) \
+		{ \
+			Z_Free(mobj->y); \
+			mobj->y = NULL; \
+		} \
+		else \
+		{ \
+			mobj->y = static_cast<char *>(Z_Realloc(mobj->y, str->len + 1, PU_LEVEL, NULL)); \
+			M_Memcpy(mobj->y, str->str, str->len + 1); \
+			mobj->y[str->len] = '\0'; \
+		} \
+		break; \
+	}
+
+#define PROP_ANGLE(x, y) \
+	case x: \
+	{ \
+		mobj->y = static_cast<angle_t>( FixedAngle(value) ); \
+		break; \
+	}
+
+#define PROP_TYPE(x, y) \
+	case x: \
+	{ \
+		if (mobj->player == NULL) \
+		{ \
+			mobjtype_t newType = mobj->y; \
+			bool success = ACS_GetMobjTypeFromString(thread->scopeMap->getString( value )->str, &newType); \
+			if (success == true) \
+			{ \
+				mobj->y = newType; \
+				mobj->info = &mobjinfo[newType]; \
+				P_SetScale(mobj, mobj->scale, false); \
+			} \
+		} \
+		break; \
+	}
+
+#define PROP_SPR(x, y) \
+	case x: \
+	{ \
+		spritenum_t newSprite = mobj->y; \
+		bool success = ACS_GetSpriteFromString(thread->scopeMap->getString( value )->str, &newSprite); \
+		if (success == true) \
+		{ \
+			mobj->y = newSprite; \
+		} \
+		break; \
+	}
+
+#define PROP_SPR2(x, y) \
+	case x: \
+	{ \
+		playersprite_t newSprite2 = static_cast<playersprite_t>(mobj->y); \
+		bool success = ACS_GetSprite2FromString(thread->scopeMap->getString( value )->str, &newSprite2); \
+		if (success == true) \
+		{ \
+			mobj->y = static_cast< decltype(mobj->y) >(newSprite2); \
+		} \
+		break; \
+	}
+
+#define PROP_STATE(x, y) \
+	case x: \
+	{ \
+		statenum_t newState = static_cast<statenum_t>(mobj->y - states); \
+		bool success = ACS_GetStateFromString(thread->scopeMap->getString( value )->str, &newState); \
+		if (success == true) \
+		{ \
+			P_SetMobjState(mobj, newState); \
+		} \
+		break; \
+	}
+
+#define PROP_SKIN(x, y) \
+	case x: \
+	{ \
+		INT32 newSkin = (mobj->skin != NULL) ? (static_cast<skin_t *>(mobj->skin))->skinnum : -1; \
+		bool success = ACS_GetSkinFromString(thread->scopeMap->getString( value )->str, &newSkin); \
+		if (success == true) \
+		{ \
+			mobj->y = (newSkin >= 0 && newSkin < numskins) ? &skins[ newSkin ] : NULL; \
+		} \
+		break; \
+	}
+
+#define PROP_COLOR(x, y) \
+	case x: \
+	{ \
+		skincolornum_t newColor = static_cast<skincolornum_t>(mobj->y); \
+		bool success = ACS_GetColorFromString(thread->scopeMap->getString( value )->str, &newColor); \
+		if (success == true) \
+		{ \
+			mobj->y = static_cast< decltype(mobj->y) >(newColor); \
+		} \
+		break; \
+	}
+
+#define PROP_MOBJ(x, y) \
+	case x: \
+	{ \
+		mobj_t *newTarget = P_FindMobjFromTID(value, NULL, NULL); \
+		P_SetTarget(&mobj->y, newTarget); \
+		break; \
+	}
+
+#define PROP_SCALE(x, y) \
+	case x: \
+	{ \
+		P_SetScale(mobj, value, false); \
+		break; \
+	}
+
+#define PROP_FLAGS(x, y) \
+	case x: \
+	{ \
+		if ((value & (MF_NOBLOCKMAP|MF_NOSECTOR)) != (mobj->y & (MF_NOBLOCKMAP|MF_NOSECTOR))) \
+		{ \
+			P_UnsetThingPosition(mobj); \
+			mobj->y = value; \
+			if ((value & MF_NOSECTOR) && sector_list) \
+			{ \
+				P_DelSeclist(sector_list); \
+				sector_list = NULL; \
+			} \
+			mobj->snext = NULL, mobj->sprev = NULL; \
+			P_SetThingPosition(mobj); \
+		} \
+		else \
+		{ \
+			mobj->y = value; \
+		} \
+		break; \
+	}
+
+		switch (property)
+		{
+			PROP_READONLY(THING_PROP_X, x)
+			PROP_READONLY(THING_PROP_Y, y)
+			PROP_READONLY(THING_PROP_Z, z)
+			PROP_TYPE(THING_PROP_TYPE, type)
+			PROP_ANGLE(THING_PROP_ANGLE, angle)
+			PROP_ANGLE(THING_PROP_PITCH, pitch)
+			PROP_ANGLE(THING_PROP_ROLL, roll)
+			PROP_ANGLE(THING_PROP_SPRITEROLL, spriteroll)
+			PROP_INT(THING_PROP_FRAME, frame)
+			PROP_SPR(THING_PROP_SPRITE, sprite)
+			PROP_SPR2(THING_PROP_SPRITE2, sprite2)
+			PROP_INT(THING_PROP_RENDERFLAGS, renderflags)
+			PROP_INT(THING_PROP_SPRITEXSCALE, spritexscale)
+			PROP_INT(THING_PROP_SPRITEYSCALE, spriteyscale)
+			PROP_INT(THING_PROP_SPRITEXOFFSET, spritexoffset)
+			PROP_INT(THING_PROP_SPRITEYOFFSET, spriteyoffset)
+			PROP_INT(THING_PROP_FLOORZ, floorz)
+			PROP_INT(THING_PROP_CEILINGZ, ceilingz)
+			PROP_READONLY(THING_PROP_RADIUS, radius)
+			PROP_READONLY(THING_PROP_HEIGHT, height)
+			PROP_INT(THING_PROP_MOMX, momx)
+			PROP_INT(THING_PROP_MOMY, momy)
+			PROP_INT(THING_PROP_MOMZ, momz)
+			PROP_INT(THING_PROP_TICS, tics)
+			PROP_STATE(THING_PROP_STATE, state)
+			PROP_FLAGS(THING_PROP_FLAGS, flags)
+			PROP_INT(THING_PROP_FLAGS2, flags2)
+			PROP_INT(THING_PROP_EFLAGS, eflags)
+			PROP_SKIN(THING_PROP_SKIN, skin)
+			PROP_COLOR(THING_PROP_COLOR, color)
+			PROP_INT(THING_PROP_HEALTH, health)
+			PROP_INT(THING_PROP_MOVEDIR, movedir)
+			PROP_INT(THING_PROP_MOVECOUNT, movecount)
+			PROP_INT(THING_PROP_REACTIONTIME, reactiontime)
+			PROP_INT(THING_PROP_THRESHOLD, threshold)
+			PROP_INT(THING_PROP_LASTLOOK, lastlook)
+			PROP_INT(THING_PROP_FRICTION, friction)
+			PROP_INT(THING_PROP_MOVEFACTOR, movefactor)
+			PROP_INT(THING_PROP_FUSE, fuse)
+			PROP_INT(THING_PROP_WATERTOP, watertop)
+			PROP_INT(THING_PROP_WATERBOTTOM, waterbottom)
+			PROP_SCALE(THING_PROP_SCALE, scale)
+			PROP_INT(THING_PROP_DESTSCALE, destscale)
+			PROP_INT(THING_PROP_SCALESPEED, scalespeed)
+			PROP_INT(THING_PROP_EXTRAVALUE1, extravalue1)
+			PROP_INT(THING_PROP_EXTRAVALUE2, extravalue2)
+			PROP_INT(THING_PROP_CUSVAL, cusval)
+			PROP_INT(THING_PROP_CVMEM, cvmem)
+			PROP_INT(THING_PROP_COLORIZED, colorized)
+			PROP_INT(THING_PROP_MIRRORED, mirrored)
+			PROP_INT(THING_PROP_SHADOWSCALE, shadowscale)
+			PROP_INT(THING_PROP_DISPOFFSET, dispoffset)
+			PROP_MOBJ(THING_PROP_TARGET, target)
+			PROP_MOBJ(THING_PROP_TRACER, tracer)
+			PROP_MOBJ(THING_PROP_HNEXT, hnext)
+			PROP_MOBJ(THING_PROP_HPREV, hprev)
+			default:
+			{
+				CONS_Alert(CONS_WARNING, "SetThingProperty type %d out of range (expected 0 - %d).\n", property, THING_PROP__MAX-1);
+				break;
+			}
+		}
+
+		mobj = P_FindMobjFromTID(tag, mobj, info->mo);
+
+#undef PROP_FLAGS
+#undef PROP_SCALE
+#undef PROP_MOBJ
+#undef PROP_COLOR
+#undef PROP_SKIN
+#undef PROP_STATE
+#undef PROP_SPR2
+#undef PROP_SPR
+#undef PROP_TYPE
+#undef PROP_ANGLE
+#undef PROP_STR
+#undef PROP_INT
+#undef PROP_READONLY
+
+	}
+
+	thread->dataStk.push(0);
+
+	return false;
+}
+
+//not needed
+#if 0
+bool CallFunc_AddMessage(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+{
+	(void)argC;
+
+	ACSVM::MapScope *map = thread->scopeMap;
+
+	CONS_Printf(map->getString(argV[0])->str);
+
+	thread->dataStk.push(0);
+
+	return false;
+}
+
+bool CallFunc_AddMessageForPlayer(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC)
+{
+	(void)argC;
+
+	ACSVM::MapScope *map = thread->scopeMap;
+
+	auto info = &static_cast<Thread *>(thread)->info;
+
+	if ((info != NULL)
+		&& (info->mo != NULL && P_MobjWasRemoved(info->mo) == false)
+		&& (info->mo->player != NULL))
+	{
+		if (ACS_ActivatorIsLocal(thread))
+			CONS_Printf(map->getString(argV[0])->str);
+	}
+
+	thread->dataStk.push(0);
+
+	return false;
+}
+#endif
diff --git a/src/acs/call-funcs.hpp b/src/acs/call-funcs.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..c0a88a862a37bec173bf48152aeb27482c7c4999
--- /dev/null
+++ b/src/acs/call-funcs.hpp
@@ -0,0 +1,115 @@
+// SONIC ROBO BLAST 2
+//-----------------------------------------------------------------------------
+// Copyright (C) 2024 by Russell's Smart Interfaces
+// Copyright (C) 2024 by Sonic Team Junior.
+// Copyright (C) 2016 by James Haley, David Hill, et al. (Team Eternity)
+// Copyright (C) 2024 by Sally "TehRealSalt" Cochenour
+// Copyright (C) 2024 by Kart Krew
+//
+// This program is free software distributed under the
+// terms of the GNU General Public License, version 2.
+// See the 'LICENSE' file for more details.
+//-----------------------------------------------------------------------------
+/// \file  call-funcs.hpp
+/// \brief Action Code Script: CallFunc instructions
+
+#ifndef __SRB2_ACS_CALL_FUNCS_HPP__
+#define __SRB2_ACS_CALL_FUNCS_HPP__
+
+#include "acsvm.hpp"
+
+/*--------------------------------------------------
+	bool CallFunc_???(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC);
+
+		These are the actual CallFuncs ran when ACS
+		is executed. Which CallFuncs are executed
+		is based on the indices from the compiled
+		data. ACS_EnvConstruct is where the link
+		between the byte code and the actual function
+		is made.
+
+	Input Arguments:-
+		thread: The ACS execution thread this action
+			is running on.
+		argV: An array of the action's arguments.
+		argC: The length of the argument array.
+
+	Return:-
+		Returns true if this function pauses the
+		thread's execution. Otherwise returns false.
+--------------------------------------------------*/
+
+bool CallFunc_Random(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC);
+bool CallFunc_ThingCount(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC);
+bool CallFunc_TagWait(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC);
+bool CallFunc_PolyWait(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC);
+bool CallFunc_CameraWait(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC);
+bool CallFunc_ChangeFloor(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC);
+bool CallFunc_ChangeCeiling(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC);
+bool CallFunc_LineSide(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC);
+bool CallFunc_ClearLineSpecial(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC);
+bool CallFunc_EndPrint(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC);
+bool CallFunc_PlayerCount(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC);
+bool CallFunc_GameType(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC);
+bool CallFunc_Timer(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC);
+bool CallFunc_IsNetworkGame(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC);
+bool CallFunc_SectorSound(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC);
+bool CallFunc_AmbientSound(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC);
+bool CallFunc_SetLineTexture(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC);
+bool CallFunc_SetLineSpecial(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC);
+bool CallFunc_ChangeSky(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC);
+bool CallFunc_ThingSound(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC);
+bool CallFunc_EndPrintBold(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC);
+bool CallFunc_PlayerTeam(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC);
+bool CallFunc_PlayerRings(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC);
+bool CallFunc_PlayerScore(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC);
+bool CallFunc_PlayerNumber(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC);
+bool CallFunc_ActivatorTID(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC);
+bool CallFunc_EndLog(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC);
+
+bool CallFunc_strcmp(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC);
+bool CallFunc_strcasecmp(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC);
+
+bool CallFunc_CountEnemies(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC);
+bool CallFunc_CountPushables(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC);
+bool CallFunc_HaveUnlockable(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC);
+bool CallFunc_PlayerSkin(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC);
+bool CallFunc_PlayerBot(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC);
+bool CallFunc_PlayerExiting(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC);
+bool CallFunc_SetObjectDye(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC);
+bool CallFunc_GetObjectDye(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC);
+bool CallFunc_PlayerEmeralds(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC);
+bool CallFunc_PlayerLap(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC);
+bool CallFunc_LowestLap(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC);
+bool CallFunc_RingSlingerMode(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC);
+bool CallFunc_CaptureTheFlagMode(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC);
+bool CallFunc_TeamGame(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC);
+bool CallFunc_ModeAttacking(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC);
+bool CallFunc_RecordAttack(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC);
+bool CallFunc_NiGHTSAttack(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC);
+
+bool CallFunc_SetLineRenderStyle(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC);
+
+bool CallFunc_MapWarp(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC);
+bool CallFunc_AddBot(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC);
+bool CallFunc_ExitLevel(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC);
+bool CallFunc_MusicPlay(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC);
+bool CallFunc_MusicStopAll(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC);
+bool CallFunc_MusicRestore(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC);
+bool CallFunc_MusicDim(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC);
+
+bool CallFunc_GetLineProperty(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC);
+bool CallFunc_SetLineProperty(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC);
+bool CallFunc_GetSideProperty(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC);
+bool CallFunc_SetSideProperty(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC);
+bool CallFunc_GetSectorProperty(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC);
+bool CallFunc_SetSectorProperty(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC);
+bool CallFunc_GetThingProperty(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC);
+bool CallFunc_SetThingProperty(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC);
+
+#if 0
+bool CallFunc_AddMessage(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC);
+bool CallFunc_AddMessageForPlayer(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC);
+#endif
+
+#endif // __SRB2_ACS_CALL_FUNCS_HPP__
diff --git a/src/acs/environment.cpp b/src/acs/environment.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..2bcaa1d88fb088587d1877f67b0308cb27273322
--- /dev/null
+++ b/src/acs/environment.cpp
@@ -0,0 +1,395 @@
+// SONIC ROBO BLAST 2
+//-----------------------------------------------------------------------------
+// Copyright (C) 2024 by Russell's Smart Interfaces
+// Copyright (C) 2024 by Sonic Team Junior.
+// Copyright (C) 2016 by James Haley, David Hill, et al. (Team Eternity)
+// Copyright (C) 2024 by Sally "TehRealSalt" Cochenour
+// Copyright (C) 2024 by Kart Krew
+//
+// This program is free software distributed under the
+// terms of the GNU General Public License, version 2.
+// See the 'LICENSE' file for more details.
+//-----------------------------------------------------------------------------
+/// \file  environment.cpp
+/// \brief Action Code Script: Environment definition
+
+#include <algorithm>
+#include <vector>
+
+#include "acsvm.hpp"
+
+#include "../doomtype.h"
+#include "../doomdef.h"
+#include "../doomstat.h"
+
+#include "../r_defs.h"
+#include "../r_state.h"
+#include "../g_game.h"
+#include "../p_spec.h"
+#include "../w_wad.h"
+#include "../z_zone.h"
+#include "../p_local.h"
+#include "../p_spec.h"
+
+#include "environment.hpp"
+#include "thread.hpp"
+#include "call-funcs.hpp"
+#include "../cxxutil.hpp"
+
+using namespace srb2::acs;
+
+Environment ACSEnv;
+
+Environment::Environment()
+{
+	ACSVM::GlobalScope *global = getGlobalScope(0);
+
+	// Activate global scope immediately, since we don't want it off.
+	// Not that we're adding any modules to it, though. :p
+	global->active = true;
+
+	// Add the data & function pointers.
+
+	// Starting with raw ACS0 codes. I'm using this classic-style
+	// format here to have a blueprint for what needs implementing,
+	// but it'd also be fine to move these to new style.
+
+	// See also:
+	// - https://doomwiki.org/wiki/ACS0_instruction_set
+	// - https://github.com/DavidPH/ACSVM/blob/master/ACSVM/CodeData.hpp
+	// - https://github.com/DavidPH/ACSVM/blob/master/ACSVM/CodeList.hpp
+
+	//  0 to 56: Implemented by ACSVM
+	addCodeDataACS0( 57, {"",        2, addCallFunc(CallFunc_Random)});
+	addCodeDataACS0( 58, {"WW",      0, addCallFunc(CallFunc_Random)});
+	addCodeDataACS0( 59, {"",        2, addCallFunc(CallFunc_ThingCount)});
+	addCodeDataACS0( 60, {"WW",      0, addCallFunc(CallFunc_ThingCount)});
+	addCodeDataACS0( 61, {"",        1, addCallFunc(CallFunc_TagWait)});
+	addCodeDataACS0( 62, {"W",       0, addCallFunc(CallFunc_TagWait)});
+	addCodeDataACS0( 63, {"",        1, addCallFunc(CallFunc_PolyWait)});
+	addCodeDataACS0( 64, {"W",       0, addCallFunc(CallFunc_PolyWait)});
+	addCodeDataACS0( 65, {"",        2, addCallFunc(CallFunc_ChangeFloor)});
+	addCodeDataACS0( 66, {"WWS",     0, addCallFunc(CallFunc_ChangeFloor)});
+	addCodeDataACS0( 67, {"",        2, addCallFunc(CallFunc_ChangeCeiling)});
+	addCodeDataACS0( 68, {"WWS",     0, addCallFunc(CallFunc_ChangeCeiling)});
+	// 69 to 79: Implemented by ACSVM
+	addCodeDataACS0( 80, {"",        0, addCallFunc(CallFunc_LineSide)});
+	// 81 to 82: Implemented by ACSVM
+	addCodeDataACS0( 83, {"",        0, addCallFunc(CallFunc_ClearLineSpecial)});
+	// 84 to 85: Implemented by ACSVM
+	addCodeDataACS0( 86, {"",        0, addCallFunc(CallFunc_EndPrint)});
+	// 87 to 89: Implemented by ACSVM
+	addCodeDataACS0( 90, {"",        0, addCallFunc(CallFunc_PlayerCount)});
+	addCodeDataACS0( 91, {"",        0, addCallFunc(CallFunc_GameType)});
+	// addCodeDataACS0( 92, {"",        0, addCallFunc(CallFunc_GameSkill)});
+	addCodeDataACS0( 93, {"",        0, addCallFunc(CallFunc_Timer)});
+	addCodeDataACS0( 94, {"",        2, addCallFunc(CallFunc_SectorSound)});
+	addCodeDataACS0( 95, {"",        2, addCallFunc(CallFunc_AmbientSound)});
+
+	addCodeDataACS0( 97, {"",        4, addCallFunc(CallFunc_SetLineTexture)});
+
+	addCodeDataACS0( 99, {"",        7, addCallFunc(CallFunc_SetLineSpecial)});
+	addCodeDataACS0(100, {"",        3, addCallFunc(CallFunc_ThingSound)});
+	addCodeDataACS0(101, {"",        0, addCallFunc(CallFunc_EndPrintBold)});
+	// Hexen p-codes end here
+
+	// Skulltag p-codes begin here
+	addCodeDataACS0(118, {"",        0, addCallFunc(CallFunc_IsNetworkGame)});
+	addCodeDataACS0(119, {"",        0, addCallFunc(CallFunc_PlayerTeam)});
+	addCodeDataACS0(120, {"",        0, addCallFunc(CallFunc_PlayerRings)});
+
+	addCodeDataACS0(122, {"",        0, addCallFunc(CallFunc_PlayerScore)});
+
+	// 136 to 137: Implemented by ACSVM
+
+	// 157: Implemented by ACSVM
+
+	// 167 to 173: Implemented by ACSVM
+	addCodeDataACS0(174, {"BB",      0, addCallFunc(CallFunc_Random)});
+	// 175 to 179: Implemented by ACSVM
+
+	// 181 to 189: Implemented by ACSVM
+
+	// 203 to 217: Implemented by ACSVM
+
+	// 225 to 243: Implemented by ACSVM
+
+	addCodeDataACS0(247, {"",        0, addCallFunc(CallFunc_PlayerNumber)});
+	addCodeDataACS0(248, {"",        0, addCallFunc(CallFunc_ActivatorTID)});
+
+	// 253: Implemented by ACSVM
+
+	// 256 to 257: Implemented by ACSVM
+
+	// 263: Implemented by ACSVM
+	addCodeDataACS0(266, {"",        2, addCallFunc(CallFunc_ChangeSky)}); // reimplements linedef type 423
+	addCodeDataACS0(270, {"",        0, addCallFunc(CallFunc_EndLog)});
+	// 273 to 275: Implemented by ACSVM
+
+	// 291 to 325: Implemented by ACSVM
+
+	// 330: Implemented by ACSVM
+
+	// 349 to 361: Implemented by ACSVM
+
+	// 363 to 381: Implemented by ACSVM
+
+	// Now for new style functions.
+	// This style is preferred for added functions
+	// that aren't mimicing one from Hexen's or ZDoom's
+	// ACS implementations.
+	addFuncDataACS0(   1, addCallFunc(CallFunc_GetLineProperty));
+	addFuncDataACS0(   2, addCallFunc(CallFunc_SetLineProperty));
+	// addFuncDataACS0(   3, addCallFunc(CallFunc_GetLineUserProperty));
+	addFuncDataACS0(   4, addCallFunc(CallFunc_GetSectorProperty));
+	addFuncDataACS0(   5, addCallFunc(CallFunc_SetSectorProperty));
+	// addFuncDataACS0(   6, addCallFunc(CallFunc_GetSectorUserProperty));
+	addFuncDataACS0(   7, addCallFunc(CallFunc_GetSideProperty));
+	addFuncDataACS0(   8, addCallFunc(CallFunc_SetSideProperty));
+	// addFuncDataACS0(   9, addCallFunc(CallFunc_GetSideUserProperty));
+	addFuncDataACS0(  10, addCallFunc(CallFunc_GetThingProperty));
+	addFuncDataACS0(  11, addCallFunc(CallFunc_SetThingProperty));
+	// addFuncDataACS0(  12, addCallFunc(CallFunc_GetThingUserProperty));
+	// addFuncDataACS0(  13, addCallFunc(CallFunc_GetPlayerProperty));
+	// addFuncDataACS0(  14, addCallFunc(CallFunc_SetPlayerProperty));
+	// addFuncDataACS0(  15, addCallFunc(CallFunc_GetPolyobjProperty));
+	// addFuncDataACS0(  16, addCallFunc(CallFunc_SetPolyobjProperty));
+
+	addFuncDataACS0( 100, addCallFunc(CallFunc_strcmp));
+	addFuncDataACS0( 101, addCallFunc(CallFunc_strcasecmp));
+
+	addFuncDataACS0( 300, addCallFunc(CallFunc_CountEnemies));
+	addFuncDataACS0( 301, addCallFunc(CallFunc_CountPushables));
+	// addFuncDataACS0( 302, addCallFunc(CallFunc_Dummy1));
+	addFuncDataACS0( 303, addCallFunc(CallFunc_HaveUnlockable));
+	addFuncDataACS0( 304, addCallFunc(CallFunc_PlayerSkin));
+	addFuncDataACS0( 305, addCallFunc(CallFunc_GetObjectDye));
+	addFuncDataACS0( 306, addCallFunc(CallFunc_PlayerEmeralds));
+	addFuncDataACS0( 307, addCallFunc(CallFunc_PlayerLap));
+	addFuncDataACS0( 308, addCallFunc(CallFunc_LowestLap));
+	addFuncDataACS0( 309, addCallFunc(CallFunc_RingSlingerMode));
+	addFuncDataACS0( 310, addCallFunc(CallFunc_TeamGame));
+	addFuncDataACS0( 311, addCallFunc(CallFunc_RecordAttack));
+	addFuncDataACS0( 312, addCallFunc(CallFunc_SetObjectDye));
+	addFuncDataACS0( 313, addCallFunc(CallFunc_CaptureTheFlagMode));
+	addFuncDataACS0( 315, addCallFunc(CallFunc_PlayerBot));
+	addFuncDataACS0( 316, addCallFunc(CallFunc_ModeAttacking));
+	addFuncDataACS0( 317, addCallFunc(CallFunc_NiGHTSAttack));
+	// addFuncDataACS0( 318, addCallFunc(CallFunc_Dummy3));
+	// addFuncDataACS0( 319, addCallFunc(CallFunc_Dummy4));
+	addFuncDataACS0( 320, addCallFunc(CallFunc_PlayerExiting));
+
+	addFuncDataACS0( 500, addCallFunc(CallFunc_CameraWait));
+	// addFuncDataACS0( 501, addCallFunc(CallFunc_Dummy5));
+	// addFuncDataACS0( 502, addCallFunc(CallFunc_Dummy6));
+	addFuncDataACS0( 503, addCallFunc(CallFunc_SetLineRenderStyle));
+	addFuncDataACS0( 504, addCallFunc(CallFunc_MapWarp));
+	addFuncDataACS0( 505, addCallFunc(CallFunc_AddBot));
+	// addFuncDataACS0( 506, addCallFunc(CallFunc_Dummy7));
+	addFuncDataACS0( 507, addCallFunc(CallFunc_ExitLevel));
+	addFuncDataACS0( 508, addCallFunc(CallFunc_MusicPlay));
+	addFuncDataACS0( 509, addCallFunc(CallFunc_MusicStopAll));
+	addFuncDataACS0( 510, addCallFunc(CallFunc_MusicRestore));
+	// addFuncDataACS0( 511, addCallFunc(CallFunc_Dummy9));
+	addFuncDataACS0( 512, addCallFunc(CallFunc_MusicDim));
+
+	// addFuncDataACS0( 600, addCallFunc(CallFunc_Dummy10));
+	// addFuncDataACS0( 601, addCallFunc(CallFunc_Dummy11));
+	// addFuncDataACS0( 602, addCallFunc(CallFunc_Dummy12));
+	// addFuncDataACS0( 603, addCallFunc(CallFunc_Dummy13));
+	// addFuncDataACS0( 604, addCallFunc(CallFunc_Dummy14));
+	// addFuncDataACS0( 605, addCallFunc(CallFunc_Dummy15));
+
+	// addFuncDataACS0( 700, addCallFunc(CallFunc_AddMessage));
+	// addFuncDataACS0( 701, addCallFunc(CallFunc_AddMessageForPlayer));
+	// addFuncDataACS0( 702, addCallFunc(CallFunc_Dummy16));
+	// addFuncDataACS0( 703, addCallFunc(CallFunc_Dummy17));
+}
+
+ACSVM::Thread *Environment::allocThread()
+{
+	return new Thread(this);
+}
+
+ACSVM::ModuleName Environment::getModuleName(char const *str, size_t len)
+{
+	ACSVM::String *name = getString(str, len);
+	lumpnum_t lump = W_CheckNumForNameInFolder(str, "ACS/");
+
+	return { name, nullptr, static_cast<size_t>(lump) };
+}
+
+void Environment::loadModule(ACSVM::Module *module)
+{
+	ACSVM::ModuleName *const name = &module->name;
+
+	size_t lumpLen = 0;
+	std::vector<ACSVM::Byte> data;
+
+	if (name->i == (size_t)LUMPERROR)
+	{
+		// No lump given for module.
+		CONS_Alert(CONS_WARNING, "Could not find ACS module \"%s\"; scripts will not function properly!\n", name->s->str);
+		return; //throw ACSVM::ReadError("file open failure");
+	}
+
+	lumpLen = W_LumpLength(name->i);
+
+	if (W_IsLumpWad(name->i) == true || lumpLen == 0)
+	{
+		CONS_Debug(DBG_SETUP, "Attempting to load ACS module from the BEHAVIOR lump of map '%s'...\n", name->s->str);
+
+		// The lump given is a virtual resource.
+		// Try to grab a BEHAVIOR lump from inside of it.
+		virtres_t *vRes = vres_GetMap(name->i);
+		auto _ = srb2::finally([vRes]() { vres_Free(vRes); });
+
+		virtlump_t *vLump = vres_Find(vRes, "BEHAVIOR");
+		if (vLump != nullptr && vLump->size > 0)
+		{
+			data.insert(data.begin(), vLump->data, vLump->data + vLump->size);
+			CONS_Debug(DBG_SETUP, "Successfully found BEHAVIOR lump.\n");
+		}
+		else
+		{
+			CONS_Debug(DBG_SETUP, "No BEHAVIOR lump found.\n");
+		}
+	}
+	else
+	{
+		CONS_Debug(DBG_SETUP, "Loading ACS module directly from lump '%s'...\n", name->s->str);
+
+		// It's a real lump.
+		ACSVM::Byte *lump = static_cast<ACSVM::Byte *>(Z_Calloc(lumpLen, PU_STATIC, nullptr));
+		auto _ = srb2::finally([lump]() { Z_Free(lump); });
+
+		W_ReadLump(name->i, lump);
+		data.insert(data.begin(), lump, lump + lumpLen);
+	}
+
+	if (data.empty() == false)
+	{
+		try
+		{
+			module->readBytecode(data.data(), data.size());
+		}
+		catch (const ACSVM::ReadError &e)
+		{
+			CONS_Alert(CONS_ERROR, "Failed to load ACS module '%s': %s\n", name->s->str, e.what());
+			throw ACSVM::ReadError("failed import");
+		}
+	}
+	else
+	{
+		// Unlike Hexen, a BEHAVIOR lump is not required.
+		// Simply ignore in this instance.
+		CONS_Debug(DBG_SETUP, "ACS module has no data, ignoring...\n");
+	}
+}
+
+bool Environment::checkTag(ACSVM::Word type, ACSVM::Word tag)
+{
+	switch (type)
+	{
+		case ACS_TAGTYPE_SECTOR:
+		{
+			INT32 secnum = -1;
+
+			TAG_ITER_SECTORS(tag, secnum)
+			{
+				sector_t *sec = &sectors[secnum];
+
+				if (sec->floordata != nullptr || sec->ceilingdata != nullptr)
+				{
+					return false;
+				}
+			}
+
+			return true;
+		}
+
+		case ACS_TAGTYPE_POLYOBJ:
+		{
+			const polyobj_t *po = Polyobj_GetForNum(tag);
+			return (po == nullptr || po->thinker == nullptr);
+		}
+
+		case ACS_TAGTYPE_CAMERA:
+		{
+			const mobj_t *camera = P_FindObjectTypeFromTag(MT_ALTVIEWMAN, tag);
+			if (camera == nullptr)
+			{
+				return true;
+			}
+
+			return (camera->tracer == nullptr || P_MobjWasRemoved(camera->tracer) == true);
+		}
+	}
+
+	return true;
+}
+
+ACSVM::Word Environment::callSpecImpl
+	(
+		ACSVM::Thread *thread, ACSVM::Word spec,
+		const ACSVM::Word *argV, ACSVM::Word argC
+	)
+{
+	auto info = &static_cast<Thread *>(thread)->info;
+	ACSVM::MapScope *const map = thread->scopeMap;
+
+	INT32 args[NUM_SCRIPT_ARGS] = {0};
+
+	char *stringargs[NUM_SCRIPT_STRINGARGS] = {0};
+	auto _ = srb2::finally(
+		[stringargs]()
+		{
+			for (int i = 0; i < NUM_SCRIPT_STRINGARGS; i++)
+			{
+				Z_Free(stringargs[i]);
+			}
+		}
+	);
+
+	activator_t *activator = static_cast<activator_t *>(Z_Calloc(sizeof(activator_t), PU_LEVEL, nullptr));
+	auto __ = srb2::finally(
+		[info, activator]()
+		{
+			if (info->thread_era == thinker_era)
+			{
+				P_SetTarget(&activator->mo, NULL);
+				Z_Free(activator);
+			}
+		}
+	);
+
+	int i = 0;
+
+	for (i = 0; i < std::min((signed)(argC), NUM_SCRIPT_STRINGARGS); i++)
+	{
+		ACSVM::String *strPtr = map->getString(argV[i]);
+
+		stringargs[i] = static_cast<char *>(Z_Malloc(strPtr->len + 1, PU_STATIC, nullptr));
+		M_Memcpy(stringargs[i], strPtr->str, strPtr->len + 1);
+	}
+
+	for (i = 0; i < std::min((signed)(argC), NUM_SCRIPT_ARGS); i++)
+	{
+		args[i] = argV[i];
+	}
+
+	P_SetTarget(&activator->mo, info->mo);
+	activator->line = info->line;
+	activator->side = info->side;
+	activator->sector = info->sector;
+	activator->po = info->po;
+	activator->fromLineSpecial = false;
+
+	P_ProcessSpecial(activator, spec, args, stringargs);
+	return 1;
+}
+
+void Environment::printKill(ACSVM::Thread *thread, ACSVM::Word type, ACSVM::Word data)
+{
+	CONS_Alert(CONS_ERROR, "ACSVM ERROR: Kill %u:%d at %lu\n", type, data, (thread->codePtr - thread->module->codeV.data() - 1));
+}
diff --git a/src/acs/environment.hpp b/src/acs/environment.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..453df89e5109f12f1e40d21de17d5e6460854629
--- /dev/null
+++ b/src/acs/environment.hpp
@@ -0,0 +1,49 @@
+// SONIC ROBO BLAST 2
+//-----------------------------------------------------------------------------
+// Copyright (C) 2024 by Russell's Smart Interfaces
+// Copyright (C) 2024 by Sonic Team Junior.
+// Copyright (C) 2016 by James Haley, David Hill, et al. (Team Eternity)
+// Copyright (C) 2024 by Sally "TehRealSalt" Cochenour
+// Copyright (C) 2024 by Kart Krew
+//
+// This program is free software distributed under the
+// terms of the GNU General Public License, version 2.
+// See the 'LICENSE' file for more details.
+//-----------------------------------------------------------------------------
+/// \file  environment.hpp
+/// \brief Action Code Script: Environment definition
+
+#ifndef __SRB2_ACS_ENVIRONMENT_HPP__
+#define __SRB2_ACS_ENVIRONMENT_HPP__
+
+#include "acsvm.hpp"
+
+namespace srb2::acs {
+
+class Environment : public ACSVM::Environment
+{
+public:
+	Environment();
+
+	virtual bool checkTag(ACSVM::Word type, ACSVM::Word tag);
+
+	virtual ACSVM::Word callSpecImpl(
+		ACSVM::Thread *thread, ACSVM::Word spec,
+		const ACSVM::Word *argV, ACSVM::Word argC
+	);
+
+	virtual ACSVM::Thread *allocThread();
+
+	virtual void printKill(ACSVM::Thread *thread, ACSVM::Word type, ACSVM::Word data);
+
+protected:
+	virtual void loadModule(ACSVM::Module *module);
+
+	virtual ACSVM::ModuleName getModuleName(char const *str, std::size_t len);
+};
+
+}
+
+extern srb2::acs::Environment ACSEnv;
+
+#endif // __SRB2_ACS_ENVIRONMENT_HPP__
diff --git a/src/acs/interface.cpp b/src/acs/interface.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..deca14f2ce0de081e1a450a699a29788703d21cb
--- /dev/null
+++ b/src/acs/interface.cpp
@@ -0,0 +1,506 @@
+// SONIC ROBO BLAST 2
+//-----------------------------------------------------------------------------
+// Copyright (C) 2024 by Russell's Smart Interfaces
+// Copyright (C) 2024 by Sonic Team Junior.
+// Copyright (C) 2016 by James Haley, David Hill, et al. (Team Eternity)
+// Copyright (C) 2024 by Sally "TehRealSalt" Cochenour
+// Copyright (C) 2024 by Kart Krew
+//
+// This program is free software distributed under the
+// terms of the GNU General Public License, version 2.
+// See the 'LICENSE' file for more details.
+//-----------------------------------------------------------------------------
+/// \file  interface.cpp
+/// \brief Action Code Script: Interface for the rest of SRB2's game logic
+
+#include <algorithm>
+#include <cstddef>
+#include <istream>
+#include <ostream>
+#include <vector>
+
+#include "../tcb_span.hpp"
+
+#include "acsvm.hpp"
+
+#include "interface.h"
+
+#include "../doomtype.h"
+#include "../doomdef.h"
+#include "../doomstat.h"
+
+#include "../r_defs.h"
+#include "../g_game.h"
+#include "../w_wad.h"
+#include "../i_system.h"
+#include "../p_saveg.h"
+
+#include "environment.hpp"
+#include "thread.hpp"
+#include "stream.hpp"
+
+#include "../cxxutil.hpp"
+
+using namespace srb2::acs;
+
+using std::size_t;
+
+/*--------------------------------------------------
+	void ACS_Init(void)
+
+		See header file for description.
+--------------------------------------------------*/
+void ACS_Init(void)
+{
+#if 0
+	// Initialize ACS on engine start-up.
+	ACSEnv = new Environment();
+	I_AddExitFunc(ACS_Shutdown);
+#endif
+}
+
+/*--------------------------------------------------
+	void ACS_Shutdown(void)
+
+		See header file for description.
+--------------------------------------------------*/
+void ACS_Shutdown(void)
+{
+#if 0
+	// Delete ACS environment.
+	delete ACSEnv;
+	ACSEnv = nullptr;
+#endif
+}
+
+/*--------------------------------------------------
+	void ACS_InvalidateMapScope(size_t mapID)
+
+		See header file for description.
+--------------------------------------------------*/
+void ACS_InvalidateMapScope(void)
+{
+	Environment *env = &ACSEnv;
+
+	ACSVM::GlobalScope *const global = env->getGlobalScope(0);
+	ACSVM::HubScope *hub = NULL;
+	ACSVM::MapScope *map = NULL;
+
+	// Conclude hub scope, even if we are not using it.
+	hub = global->getHubScope(0);
+	hub->reset();
+
+	// Conclude current map scope.
+	map = hub->getMapScope(0); // This is where you'd put in mapID if you add hub support.
+	map->reset();
+}
+
+/*--------------------------------------------------
+	void ACS_LoadLevelScripts(size_t mapID)
+
+		See header file for description.
+--------------------------------------------------*/
+void ACS_LoadLevelScripts(size_t mapID)
+{
+	Environment *env = &ACSEnv;
+
+	ACSVM::GlobalScope *const global = env->getGlobalScope(0);
+	ACSVM::HubScope *hub = nullptr;
+	ACSVM::MapScope *map = nullptr;
+
+	std::vector<ACSVM::Module *> modules;
+
+	// Just some notes on how Hexen's scopes work, if anyone
+	// intends to implement proper hub logic:
+
+	// The integer is an ID for which hub / map it is,
+	// and instead sets active according to which ones
+	// should run, since you can go between them.
+
+	// But I didn't intend on implementing these features,
+	// since hubs aren't planned for Ring Racers (although
+	// they might be useful for SRB2), and I intentionally
+	// avoided implementing global ACS (since Lua would be
+	// a better language to do that kind of code).
+
+	// Since we literally only are using map scope, we can
+	// just free everything between every level. But if
+	// hubs are to be implemented, this logic would need
+	// to be far more sophisticated.
+
+	// Extra note regarding the commented out ->reset()'s:
+	// This is too late! That needs to be done before
+	// PU_LEVEL is purged. Call ACS_InvalidateMapScope
+	// to take care of that. Those lines are left in
+	// only as a warning to future code spelunkers.
+
+	// Restart hub scope, even if we are not using it.
+	hub = global->getHubScope(0);
+	//hub->reset();
+	hub->active = true;
+
+	// Start up new map scope.
+	map = hub->getMapScope(0); // This is where you'd put in mapID if you add hub support.
+	//map->reset();
+	map->active = true;
+
+	// Insert BEHAVIOR lump into the list.
+	{
+		const char *maplumpname = G_BuildMapName(mapID);
+
+		ACSVM::ModuleName name = ACSVM::ModuleName(
+			env->getString( maplumpname ),
+			nullptr,
+			W_CheckNumForMap(maplumpname)
+		);
+
+		modules.push_back(env->getModule(name));
+	}
+
+	if (modules.empty() == false)
+	{
+		// Register the modules with map scope.
+		map->addModules(modules.data(), modules.size());
+	}
+}
+
+/*--------------------------------------------------
+	void ACS_RunLevelStartScripts(void)
+
+		See header file for description.
+--------------------------------------------------*/
+void ACS_RunLevelStartScripts(void)
+{
+	Environment *env = &ACSEnv;
+
+	ACSVM::GlobalScope *const global = env->getGlobalScope(0);
+	ACSVM::HubScope *const hub = global->getHubScope(0);
+	ACSVM::MapScope *const map = hub->getMapScope(0);
+
+	map->scriptStartType(ACS_ST_OPEN, {});
+}
+
+/*--------------------------------------------------
+	void ACS_RunPlayerRespawnScript(player_t *player)
+
+		See header file for description.
+--------------------------------------------------*/
+void ACS_RunPlayerRespawnScript(player_t *player)
+{
+	Environment *env = &ACSEnv;
+
+	ACSVM::GlobalScope *const global = env->getGlobalScope(0);
+	ACSVM::HubScope *const hub = global->getHubScope(0);
+	ACSVM::MapScope *const map = hub->getMapScope(0);
+
+	ACSVM::MapScope::ScriptStartInfo scriptInfo;
+	ThreadInfo info;
+
+	P_SetTarget(&info.mo, player->mo);
+
+	scriptInfo.info = &info;
+
+	map->scriptStartTypeForced(ACS_ST_RESPAWN, scriptInfo);
+}
+
+/*--------------------------------------------------
+	void ACS_RunPlayerDeathScript(player_t *player)
+
+		See header file for description.
+--------------------------------------------------*/
+void ACS_RunPlayerDeathScript(player_t *player)
+{
+	Environment *env = &ACSEnv;
+
+	ACSVM::GlobalScope *const global = env->getGlobalScope(0);
+	ACSVM::HubScope *const hub = global->getHubScope(0);
+	ACSVM::MapScope *const map = hub->getMapScope(0);
+
+	ACSVM::MapScope::ScriptStartInfo scriptInfo;
+	ThreadInfo info;
+
+	P_SetTarget(&info.mo, player->mo);
+
+	scriptInfo.info = &info;
+
+	map->scriptStartTypeForced(ACS_ST_DEATH, scriptInfo);
+}
+
+/*--------------------------------------------------
+	void ACS_RunPlayerEnterScript(player_t *player)
+
+		See header file for description.
+--------------------------------------------------*/
+void ACS_RunPlayerEnterScript(player_t *player)
+{
+	Environment *env = &ACSEnv;
+
+	ACSVM::GlobalScope *const global = env->getGlobalScope(0);
+	ACSVM::HubScope *const hub = global->getHubScope(0);
+	ACSVM::MapScope *const map = hub->getMapScope(0);
+
+	ACSVM::MapScope::ScriptStartInfo scriptInfo;
+	ThreadInfo info;
+
+	P_SetTarget(&info.mo, player->mo);
+
+	scriptInfo.info = &info;
+
+	map->scriptStartTypeForced(ACS_ST_ENTER, scriptInfo);
+}
+
+/*--------------------------------------------------
+	void ACS_RunPlayerFinishScript(player_t *player)
+
+		See header file for description.
+--------------------------------------------------*/
+void ACS_RunPlayerFinishScript(player_t *player)
+{
+	Environment *env = &ACSEnv;
+
+	ACSVM::GlobalScope *const global = env->getGlobalScope(0);
+	ACSVM::HubScope *const hub = global->getHubScope(0);
+	ACSVM::MapScope *const map = hub->getMapScope(0);
+
+	ACSVM::MapScope::ScriptStartInfo scriptInfo;
+	ThreadInfo info;
+
+	P_SetTarget(&info.mo, player->mo);
+
+	scriptInfo.info = &info;
+
+	map->scriptStartTypeForced(ACS_ST_FINISH, scriptInfo);
+}
+
+/*--------------------------------------------------
+	void ACS_RunGameOverScript(void)
+
+		See header file for description.
+--------------------------------------------------*/
+void ACS_RunGameOverScript(void)
+{
+	Environment *env = &ACSEnv;
+
+	ACSVM::GlobalScope *const global = env->getGlobalScope(0);
+	ACSVM::HubScope *const hub = global->getHubScope(0);
+	ACSVM::MapScope *const map = hub->getMapScope(0);
+
+	map->scriptStartType(ACS_ST_GAMEOVER, {});
+}
+
+/*--------------------------------------------------
+	void ACS_Tick(void)
+
+		See header file for description.
+--------------------------------------------------*/
+void ACS_Tick(void)
+{
+	Environment *env = &ACSEnv;
+
+	if (env->hasActiveThread() == true)
+	{
+		env->exec();
+	}
+}
+
+/*--------------------------------------------------
+	static std::vector<ACSVM::Word> ACS_MixArgs(tcb::span<const INT32> args, tcb::span<const char* const> stringArgs)
+
+		Convert strings to ACS arguments and position them
+		correctly among integer arguments.
+
+	Input Arguments:-
+		args: Integer arguments.
+		stringArgs: C string arguments.
+
+	Return:-
+		Final argument vector.
+--------------------------------------------------*/
+static std::vector<ACSVM::Word> ACS_MixArgs(tcb::span<const INT32> args, tcb::span<const char* const> stringArgs)
+{
+	std::vector<ACSVM::Word> argV;
+	size_t first = std::min(args.size(), stringArgs.size());
+
+	auto new_string = [env = &ACSEnv](const char* str) -> ACSVM::Word { return ~env->getString(str, strlen(str))->idx; };
+
+	for (size_t i = 0; i < first; ++i)
+	{
+		// args[i] must be 0.
+		//
+		// If ACS_Execute is called from ACS, stringargs[i]
+		// will always be set, because there is no
+		// differentiation between integers and strings on
+		// arguments passed to a function. In this case,
+		// string arguments already exist in the ACS string
+		// table beforehand (and set in args[i]), so no
+		// conversion is required here.
+		//
+		// If ACS_Execute is called from a map line special,
+		// args[i] may be left unset (0), while stringArgs[i]
+		// is set. In this case, conversion to ACS string
+		// table is necessary.
+		argV.push_back(!args[i] && stringArgs[i] ? new_string(stringArgs[i]) : args[i]);
+	}
+
+	for (size_t i = first; i < args.size(); ++i)
+	{
+		argV.push_back(args[i]);
+	}
+
+	for (size_t i = first; i < stringArgs.size(); ++i)
+	{
+		argV.push_back(new_string(stringArgs[i] ? stringArgs[i] : ""));
+	}
+
+	return argV;
+}
+
+/*--------------------------------------------------
+	boolean ACS_Execute(const char *name, const INT32 *args, size_t numArgs, const char *const *stringArgs, size_t numStringArgs, activator_t *activator)
+
+		See header file for description.
+--------------------------------------------------*/
+boolean ACS_Execute(const char *name, const INT32 *args, size_t numArgs, const char *const *stringArgs, size_t numStringArgs, activator_t *activator)
+{
+	Environment *env = &ACSEnv;
+
+	ACSVM::GlobalScope *const global = env->getGlobalScope(0);
+	ACSVM::HubScope *const hub = global->getHubScope(0);
+	ACSVM::MapScope *const map = hub->getMapScope(0);
+	ACSVM::ScopeID scope{global->id, hub->id, map->id};
+
+	ThreadInfo info{activator};
+
+	ACSVM::String *script = env->getString(name, strlen(name));
+	std::vector<ACSVM::Word> argV = ACS_MixArgs(tcb::span {args, numArgs}, tcb::span {stringArgs, numStringArgs});
+	return map->scriptStart(script, scope, {argV.data(), argV.size(), &info});
+}
+
+/*--------------------------------------------------
+	boolean ACS_ExecuteAlways(const char *name, const INT32 *args, size_t numArgs, const char *const *stringArgs, size_t numStringArgs, activator_t *activator)
+
+		See header file for description.
+--------------------------------------------------*/
+boolean ACS_ExecuteAlways(const char *name, const INT32 *args, size_t numArgs, const char *const *stringArgs, size_t numStringArgs, activator_t *activator)
+{
+	Environment *env = &ACSEnv;
+
+	ACSVM::GlobalScope *const global = env->getGlobalScope(0);
+	ACSVM::HubScope *const hub = global->getHubScope(0);
+	ACSVM::MapScope *const map = hub->getMapScope(0);
+	ACSVM::ScopeID scope{global->id, hub->id, map->id};
+
+	ThreadInfo info{activator};
+
+	ACSVM::String *script = env->getString(name, strlen(name));
+	std::vector<ACSVM::Word> argV = ACS_MixArgs(tcb::span {args, numArgs}, tcb::span {stringArgs, numStringArgs});
+	return map->scriptStartForced(script, scope, {argV.data(), argV.size(), &info});
+}
+
+/*--------------------------------------------------
+	boolean ACS_ExecuteResult(const char *name, const INT32 *args, size_t numArgs, activator_t *activator)
+
+		See header file for description.
+--------------------------------------------------*/
+boolean ACS_ExecuteResult(const char *name, const INT32 *args, size_t numArgs, activator_t *activator)
+{
+	Environment *env = &ACSEnv;
+
+	ACSVM::GlobalScope *const global = env->getGlobalScope(0);
+	ACSVM::HubScope *const hub = global->getHubScope(0);
+	ACSVM::MapScope *const map = hub->getMapScope(0);
+
+	ThreadInfo info{activator};
+
+	ACSVM::String *script = env->getString(name, strlen(name));
+	return map->scriptStartResult(script, {reinterpret_cast<const ACSVM::Word *>(args), numArgs, &info});
+}
+
+/*--------------------------------------------------
+	boolean ACS_Suspend(const char *name)
+
+		See header file for description.
+--------------------------------------------------*/
+boolean ACS_Suspend(const char *name)
+{
+	Environment *env = &ACSEnv;
+
+	ACSVM::GlobalScope *const global = env->getGlobalScope(0);
+	ACSVM::HubScope *const hub = global->getHubScope(0);
+	ACSVM::MapScope *const map = hub->getMapScope(0);
+	ACSVM::ScopeID scope{global->id, hub->id, map->id};
+
+	ACSVM::String *script = env->getString(name, strlen(name));
+	return map->scriptPause(script, scope);
+}
+
+/*--------------------------------------------------
+	boolean ACS_Terminate(const char *name)
+
+		See header file for description.
+--------------------------------------------------*/
+boolean ACS_Terminate(const char *name)
+{
+	Environment *env = &ACSEnv;
+
+	ACSVM::GlobalScope *const global = env->getGlobalScope(0);
+	ACSVM::HubScope *const hub = global->getHubScope(0);
+	ACSVM::MapScope *const map = hub->getMapScope(0);
+	ACSVM::ScopeID scope{global->id, hub->id, map->id};
+
+	ACSVM::String *script = env->getString(name, strlen(name));
+	return map->scriptStop(script, scope);
+}
+
+/*--------------------------------------------------
+	void ACS_Archive(savebuffer_t *save)
+
+		See header file for description.
+--------------------------------------------------*/
+void ACS_Archive(savebuffer_t *save)
+{
+	Environment *env = &ACSEnv;
+
+	SaveBuffer buffer{save};
+	std::ostream stream{&buffer};
+	ACSVM::Serial serial{stream};
+
+	// Enable debug signatures.
+	serial.signs = true;
+
+	try
+	{
+		serial.saveHead();
+		env->saveState(serial);
+		serial.saveTail();
+	}
+	catch (ACSVM::SerialError const &e)
+	{
+		I_Error("ACS_Archive: %s\n", e.what());
+	}
+}
+
+/*--------------------------------------------------
+	void ACS_UnArchive(savebuffer_t *save)
+
+		See header file for description.
+--------------------------------------------------*/
+void ACS_UnArchive(savebuffer_t *save)
+{
+	Environment *env = &ACSEnv;
+
+	SaveBuffer buffer{save};
+	std::istream stream{&buffer};
+	ACSVM::Serial serial{stream};
+
+	try
+	{
+		serial.loadHead();
+		env->loadState(serial);
+		serial.loadTail();
+	}
+	catch (ACSVM::SerialError const &e)
+	{
+		I_Error("ACS_UnArchive: %s\n", e.what());
+	}
+}
diff --git a/src/acs/interface.h b/src/acs/interface.h
new file mode 100644
index 0000000000000000000000000000000000000000..bdca8a64486099087ed0643a54596f8d3e92d075
--- /dev/null
+++ b/src/acs/interface.h
@@ -0,0 +1,321 @@
+// SONIC ROBO BLAST 2
+//-----------------------------------------------------------------------------
+// Copyright (C) 2024 by Russell's Smart Interfaces
+// Copyright (C) 2024 by Sonic Team Junior.
+// Copyright (C) 2016 by James Haley, David Hill, et al. (Team Eternity)
+// Copyright (C) 2024 by Sally "TehRealSalt" Cochenour
+// Copyright (C) 2024 by Kart Krew
+//
+// This program is free software distributed under the
+// terms of the GNU General Public License, version 2.
+// See the 'LICENSE' file for more details.
+//-----------------------------------------------------------------------------
+/// \file  interface.h
+/// \brief Action Code Script: Interface for the rest of SRB2's game logic
+
+#ifndef __SRB2_ACS_INTERFACE_H__
+#define __SRB2_ACS_INTERFACE_H__
+
+#include "../doomtype.h"
+#include "../doomdef.h"
+#include "../doomstat.h"
+
+#include "../p_spec.h"
+#include "../p_saveg.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/*--------------------------------------------------
+	void ACS_Init(void);
+
+		Initializes the ACS environment. Handles creating
+		the VM, initializing its hooks, storing the
+		pointer for future reference, and adding the
+		shutdown function.
+--------------------------------------------------*/
+
+void ACS_Init(void);
+
+
+/*--------------------------------------------------
+	void ACS_Shutdown(void);
+
+		Frees the ACS environment, for when the game
+		is exited.
+--------------------------------------------------*/
+
+void ACS_Shutdown(void);
+
+
+/*--------------------------------------------------
+	void ACS_InvalidateMapScope(size_t mapID);
+
+		Resets the ACS hub and map scopes to remove
+		existing running scripts, without starting
+		any new scripts.
+
+	Input Arguments:-
+		None
+
+	Return:-
+		None
+--------------------------------------------------*/
+
+void ACS_InvalidateMapScope(void);
+
+
+/*--------------------------------------------------
+	void ACS_LoadLevelScripts(size_t mapID);
+
+		Resets the ACS hub and map scopes to remove
+		existing running scripts, and inserts the new
+		level's ACS modules (BEHAVIOR lump) into
+		the environment.
+
+	Input Arguments:-
+		mapID: The map's number to read the BEHAVIOR
+			lump of.
+
+	Return:-
+		None
+--------------------------------------------------*/
+
+void ACS_LoadLevelScripts(size_t mapID);
+
+
+/*--------------------------------------------------
+	void ACS_RunLevelStartScripts(void);
+
+		Runs the map's special scripts for opening
+		the level, and for all players to enter
+		the game.
+--------------------------------------------------*/
+
+void ACS_RunLevelStartScripts(void);
+
+
+/*--------------------------------------------------
+	void ACS_RunPlayerRespawnScript(player_t *player);
+
+		Runs the map's special script for a player
+		respawning.
+
+	Input Arguments:-
+		player: The player to run the script for.
+
+	Return:-
+		None
+--------------------------------------------------*/
+
+void ACS_RunPlayerRespawnScript(player_t *player);
+
+
+/*--------------------------------------------------
+	void ACS_RunPlayerDeathScript(player_t *player);
+
+		Runs the map's special script for a player
+		dying.
+
+	Input Arguments:-
+		player: The player to run the script for.
+
+	Return:-
+		None
+--------------------------------------------------*/
+
+void ACS_RunPlayerDeathScript(player_t *player);
+
+
+/*--------------------------------------------------
+	void ACS_RunPlayerEnterScript(player_t *player);
+
+		Runs the map's special script for a player
+		entering the game.
+
+	Input Arguments:-
+		player: The player to run the script for.
+
+	Return:-
+		None
+--------------------------------------------------*/
+
+void ACS_RunPlayerEnterScript(player_t *player);
+
+
+/*--------------------------------------------------
+	void ACS_RunPlayerFinishScript(player_t *player);
+
+		Runs the map's special script for a player
+		finishing (P_DoPlayerExit).
+
+	Input Arguments:-
+		player: The player to run the script for.
+
+	Return:-
+		None
+--------------------------------------------------*/
+
+void ACS_RunPlayerFinishScript(player_t *player);
+
+
+/*--------------------------------------------------
+	void ACS_RunGameOverScript(void);
+
+		Runs the map's special scripts for exiting
+		the level, due to a losing condition and
+		without any extra lives to retry.
+--------------------------------------------------*/
+
+void ACS_RunGameOverScript(void);
+
+
+/*--------------------------------------------------
+	void ACS_Tick(void);
+
+		Executes all of the ACS environment's
+		currently active threads.
+--------------------------------------------------*/
+
+void ACS_Tick(void);
+
+
+/*--------------------------------------------------
+	boolean ACS_Execute(const INT32 *args, size_t numArgs, const char *const *stringArgs, size_t numStringArgs, activator_t *activator);
+
+		Runs an ACS script by its string name.
+		Only one instance of the script will run at
+		a time with this method.
+
+	Input Arguments:-
+		name: Script string to run.
+		args: Array of the input arguments.
+			Strings should be transformed into
+			ACSVM string IDs.
+		numArgs: Number of input arguments.
+		stringArgs: Array of input string arguments.
+		numStringArgs: Number of input string arguments.
+		activator: Container for information on what
+			activated this script.
+
+	Return:-
+		true if we were able to run the script, otherwise false.
+--------------------------------------------------*/
+
+boolean ACS_Execute(const char *name, const INT32 *args, size_t numArgs, const char *const *stringArgs, size_t numStringArgs, activator_t *activator);
+
+
+/*--------------------------------------------------
+	boolean ACS_ExecuteAlways(const INT32 *args, size_t numArgs, const char *const *stringArgs, size_t numStringArgs, activator_t *activator)
+
+		Runs an ACS script by its string name.
+		If the script is already running, this method
+		will create another instance of the script.
+		(Suspend and Terminate cannot be used, however.)
+
+	Input Arguments:-
+		name: Script string to run.
+		args: Array of the input arguments.
+			Strings should be transformed into
+			ACSVM string IDs.
+		numArgs: Number of input arguments.
+		stringArgs: Array of input string arguments.
+		numStringArgs: Number of input string arguments.
+		activator: Container for information on what
+			activated this script.
+
+	Return:-
+		true if we were able to run the script, otherwise false.
+--------------------------------------------------*/
+
+boolean ACS_ExecuteAlways(const char *name, const INT32 *args, size_t numArgs, const char *const *stringArgs, size_t numStringArgs, activator_t *activator);
+
+
+/*--------------------------------------------------
+	INT32 ACS_ExecuteResult(const char *name, const INT32 *args, size_t numArgs, activator_t *activator)
+
+		Runs an ACS script by its string name.
+		Will return the scripts special result
+		value, if set.
+
+	Input Arguments:-
+		name: Script string to run.
+		args: Array of the input arguments.
+			Strings should be transformed into
+			ACSVM string IDs.
+		numArgs: Number of input arguments.
+		activator: Container for information on what
+			activated this script.
+
+	Return:-
+		true if we were able to run the script, otherwise false.
+--------------------------------------------------*/
+
+INT32 ACS_ExecuteResult(const char *name, const INT32 *args, size_t numArgs, activator_t *activator);
+
+
+/*--------------------------------------------------
+	boolean ACS_Suspend(const char *name);
+
+		Pauses an ACS script by its string name.
+
+	Input Arguments:-
+		name: Script string to pause.
+
+	Return:-
+		true if we were able to pause the script, otherwise false.
+--------------------------------------------------*/
+
+boolean ACS_Suspend(const char *name);
+
+
+/*--------------------------------------------------
+	boolean ACS_Terminate(const char *name);
+
+		Stops an ACS script by its string name.
+
+	Input Arguments:-
+		name: Script string to stop.
+
+	Return:-
+		true if we were able to stop the script, otherwise false.
+--------------------------------------------------*/
+
+boolean ACS_Terminate(const char *name);
+
+
+/*--------------------------------------------------
+	void ACS_Archive(savebuffer_t *save);
+
+		Saves the ACS VM state into a save buffer.
+
+	Input Arguments:-
+		save: Pointer to the save buffer from P_SaveNetGame.
+
+	Return:-
+		None
+--------------------------------------------------*/
+
+void ACS_Archive(savebuffer_t *save);
+
+
+/*--------------------------------------------------
+	void ACS_UnArchive(savebuffer_t *save);
+
+		Loads the ACS VM state from a save buffer.
+
+	Input Arguments:-
+		save: Pointer to the save buffer from P_LoadNetGame.
+
+	Return:-
+		None
+--------------------------------------------------*/
+
+void ACS_UnArchive(savebuffer_t *save);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif // __SRB2_ACS_INTERFACE_H__
diff --git a/src/acs/stream.cpp b/src/acs/stream.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..85e070587c6aaf1799f5c069eb975f783759f113
--- /dev/null
+++ b/src/acs/stream.cpp
@@ -0,0 +1,71 @@
+// SONIC ROBO BLAST 2
+//-----------------------------------------------------------------------------
+// Copyright (C) 2024 by Russell's Smart Interfaces
+// Copyright (C) 2024 by Sonic Team Junior.
+// Copyright (C) 2016 by James Haley, David Hill, et al. (Team Eternity)
+// Copyright (C) 2024 by Sally "TehRealSalt" Cochenour
+// Copyright (C) 2024 by Kart Krew
+//
+// This program is free software distributed under the
+// terms of the GNU General Public License, version 2.
+// See the 'LICENSE' file for more details.
+//-----------------------------------------------------------------------------
+/// \file  stream.cpp
+/// \brief Action Code Script: Dummy stream buffer to
+///        interact with P_Save/LoadNetGame
+
+// TODO? Maybe untie this file from ACS?
+
+#include <istream>
+#include <ostream>
+#include <streambuf>
+
+#include "../doomtype.h"
+#include "../doomdef.h"
+#include "../doomstat.h"
+
+#include "../p_saveg.h"
+
+#include "stream.hpp"
+#include "../cxxutil.hpp"
+
+using namespace srb2::acs;
+
+SaveBuffer::SaveBuffer(savebuffer_t *save_) :
+	save{save_}
+{
+}
+
+SaveBuffer::int_type SaveBuffer::overflow(SaveBuffer::int_type ch)
+{
+	if (save->p == save->end)
+	{
+		return traits_type::eof();
+	}
+
+	*save->p = static_cast<UINT8>(ch);
+	save->p++;
+
+	return ch;
+}
+
+SaveBuffer::int_type SaveBuffer::underflow()
+{
+	if (save->p == save->end)
+	{
+		return traits_type::eof();
+	}
+
+	UINT8 ret = *save->p;
+	save->p++;
+
+	// Allow the streambuf internal funcs to work
+	buf[0] = ret;
+	setg(
+		reinterpret_cast<SaveBuffer::char_type *>(buf),
+		reinterpret_cast<SaveBuffer::char_type *>(buf),
+		reinterpret_cast<SaveBuffer::char_type *>(buf + 1)
+	);
+
+	return static_cast<SaveBuffer::int_type>(ret);
+}
diff --git a/src/acs/stream.hpp b/src/acs/stream.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..e18be2c46d24116b3e955d2a77774e5a0364cff0
--- /dev/null
+++ b/src/acs/stream.hpp
@@ -0,0 +1,50 @@
+// SONIC ROBO BLAST 2
+//-----------------------------------------------------------------------------
+// Copyright (C) 2024 by Russell's Smart Interfaces
+// Copyright (C) 2024 by Sonic Team Junior.
+// Copyright (C) 2016 by James Haley, David Hill, et al. (Team Eternity)
+// Copyright (C) 2024 by Sally "TehRealSalt" Cochenour
+// Copyright (C) 2024 by Kart Krew
+//
+// This program is free software distributed under the
+// terms of the GNU General Public License, version 2.
+// See the 'LICENSE' file for more details.
+//-----------------------------------------------------------------------------
+/// \file  stream.hpp
+/// \brief Action Code Script: Dummy stream buffer to
+///        interact with P_Save/LoadNetGame
+
+// TODO? Maybe untie this file from ACS?
+
+#ifndef __SRB2_ACS_STREAM_HPP__
+#define __SRB2_ACS_STREAM_HPP__
+
+#include <streambuf>
+
+#include "acsvm.hpp"
+
+extern "C" {
+#include "../doomtype.h"
+#include "../doomdef.h"
+#include "../doomstat.h"
+#include "../p_saveg.h"
+}
+
+namespace srb2::acs {
+
+class SaveBuffer : public std::streambuf
+{
+public:
+	savebuffer_t *save;
+	UINT8 buf[1];
+
+	explicit SaveBuffer(savebuffer_t *save_);
+
+private:
+	virtual int_type overflow(int_type ch);
+	virtual int_type underflow();
+};
+
+}
+
+#endif // __SRB2_ACS_STREAM_HPP__
diff --git a/src/acs/thread.cpp b/src/acs/thread.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..af47bb2452e9ea8f5616bec3fe64ffce8064007a
--- /dev/null
+++ b/src/acs/thread.cpp
@@ -0,0 +1,94 @@
+// SONIC ROBO BLAST 2
+//-----------------------------------------------------------------------------
+// Copyright (C) 2024 by Russell's Smart Interfaces
+// Copyright (C) 2024 by Sonic Team Junior.
+// Copyright (C) 2016 by James Haley, David Hill, et al. (Team Eternity)
+// Copyright (C) 2024 by Sally "TehRealSalt" Cochenour
+// Copyright (C) 2024 by Kart Krew
+//
+// This program is free software distributed under the
+// terms of the GNU General Public License, version 2.
+// See the 'LICENSE' file for more details.
+//-----------------------------------------------------------------------------
+/// \file  thread.cpp
+/// \brief Action Code Script: Thread definition
+
+#include "acsvm.hpp"
+
+#include "thread.hpp"
+
+#include "../doomtype.h"
+#include "../doomdef.h"
+#include "../doomstat.h"
+
+#include "../p_saveg.h"
+#include "../p_tick.h"
+#include "../p_local.h"
+#include "../r_defs.h"
+#include "../r_state.h"
+#include "../p_polyobj.h"
+
+using namespace srb2::acs;
+
+void Thread::start(
+	ACSVM::Script *script, ACSVM::MapScope *map,
+	const ACSVM::ThreadInfo *infoPtr, const ACSVM::Word *argV, ACSVM::Word argC)
+{
+	ACSVM::Thread::start(script, map, infoPtr, argV, argC);
+
+	if (infoPtr != nullptr)
+	{
+		info = *static_cast<const ThreadInfo *>(infoPtr);
+	}
+	else
+	{
+		info = {};
+	}
+
+	result = 1;
+}
+
+void Thread::stop()
+{
+	ACSVM::Thread::stop();
+	info = {};
+}
+
+void Thread::saveState(ACSVM::Serial &serial) const
+{
+	ACSVM::Thread::saveState(serial);
+
+	ACSVM::WriteVLN<size_t>(serial, (info.mo != nullptr && P_MobjWasRemoved(info.mo) == false) ? (info.mo->mobjnum) : 0);
+	ACSVM::WriteVLN<size_t>(serial, (info.line != nullptr) ? ((info.line - lines) + 1) : 0);
+	ACSVM::WriteVLN<size_t>(serial, info.side);
+	ACSVM::WriteVLN<size_t>(serial, (info.sector != nullptr) ? ((info.sector - sectors) + 1) : 0);
+	ACSVM::WriteVLN<size_t>(serial, (info.po != nullptr) ? ((info.po - PolyObjects) + 1) : 0);
+}
+
+void Thread::loadState(ACSVM::Serial &serial)
+{
+	ACSVM::Thread::loadState(serial);
+
+	UINT32 temp = static_cast<UINT32>(ACSVM::ReadVLN<size_t>(serial));
+
+	if (temp != 0)
+	{
+		info.mo = nullptr;
+
+		if (P_SetTarget(&info.mo, P_FindNewPosition(temp)) == nullptr)
+		{
+			CONS_Debug(DBG_GAMELOGIC, "info.mo not found for ACS thread\n"); // todo: identify which thread
+		}
+	}
+
+	size_t lineIndex = ACSVM::ReadVLN<size_t>(serial);
+	info.line = (lineIndex != 0) ? (&lines[lineIndex - 1]) : nullptr;
+
+	info.side = static_cast<UINT8>(ACSVM::ReadVLN<size_t>(serial));
+
+	size_t sectorIndex = ACSVM::ReadVLN<size_t>(serial);
+	info.sector = (sectorIndex != 0) ? (&sectors[sectorIndex - 1]) : nullptr;
+
+	size_t polyIndex = ACSVM::ReadVLN<size_t>(serial);
+	info.po = (polyIndex != 0) ? (&PolyObjects[polyIndex - 1]) : nullptr;
+}
diff --git a/src/acs/thread.hpp b/src/acs/thread.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..1b52780fe47c9c7327a0a2f72457df3a5a923008
--- /dev/null
+++ b/src/acs/thread.hpp
@@ -0,0 +1,146 @@
+// SONIC ROBO BLAST 2
+//-----------------------------------------------------------------------------
+// Copyright (C) 2024 by Russell's Smart Interfaces
+// Copyright (C) 2024 by Sonic Team Junior.
+// Copyright (C) 2016 by James Haley, David Hill, et al. (Team Eternity)
+// Copyright (C) 2024 by Sally "TehRealSalt" Cochenour
+// Copyright (C) 2024 by Kart Krew
+//
+// This program is free software distributed under the
+// terms of the GNU General Public License, version 2.
+// See the 'LICENSE' file for more details.
+//-----------------------------------------------------------------------------
+/// \file  thread.hpp
+/// \brief Action Code Script: Thread definition
+
+#ifndef __SRB2_ACS_THREAD_HPP__
+#define __SRB2_ACS_THREAD_HPP__
+
+#include "acsvm.hpp"
+
+extern "C" {
+#include "../doomtype.h"
+#include "../doomdef.h"
+#include "../doomstat.h"
+#include "../p_tick.h"
+#include "../r_defs.h"
+#include "../r_state.h"
+#include "../p_spec.h"
+}
+
+namespace srb2::acs {
+
+//
+// Special global script types.
+//
+enum acs_scriptType_e
+{
+	ACS_ST_OPEN			=  1, // OPEN: Runs once when the level starts.
+	ACS_ST_RESPAWN		=  2, // RESPAWN: Runs when a player respawns.
+	ACS_ST_DEATH		=  3, // DEATH: Runs when a player dies.
+	ACS_ST_ENTER		=  4, // ENTER: Runs when a player enters the game; both on start of the level, and when un-spectating.
+	ACS_ST_GAMEOVER		=  5, // GAMEOVER: Runs when the level ends due to a losing condition and no player has an extra life.
+	ACS_ST_FINISH		=  6, // FINISH: Runs when a player finishes
+};
+
+//
+// Script "waiting on tag" types.
+//
+enum acs_tagType_e
+{
+	ACS_TAGTYPE_POLYOBJ,
+	ACS_TAGTYPE_SECTOR,
+	ACS_TAGTYPE_CAMERA,
+};
+
+class ThreadInfo : public ACSVM::ThreadInfo
+{
+public:
+	UINT32 thread_era;			// If equal to thinker_era, mobj pointers are safe.
+	mobj_t *mo;					// Object that activated this thread.
+	line_t *line;				// Linedef that activated this thread.
+	UINT8 side;					// Front / back side of said linedef.
+	sector_t *sector;			// Sector that activated this thread.
+	polyobj_t *po;				// Polyobject that activated this thread.
+	bool fromLineSpecial;		// Called from P_ProcessLineSpecial.
+
+	ThreadInfo() :
+		thread_era { thinker_era },
+		mo{ nullptr },
+		line{ nullptr },
+		side{ 0 },
+		sector{ nullptr },
+		po{ nullptr },
+		fromLineSpecial{ false }
+	{
+	}
+
+	ThreadInfo(const ThreadInfo &info) :
+		thread_era { thinker_era },
+		mo{ nullptr },
+		line{ info.line },
+		side{ info.side },
+		sector{ info.sector },
+		po{ info.po },
+		fromLineSpecial{ info.fromLineSpecial }
+	{
+		P_SetTarget(&mo, info.mo);
+	}
+
+	ThreadInfo(const activator_t *activator) :
+		thread_era { thinker_era },
+		mo{ nullptr },
+		line{ activator->line },
+		side{ activator->side },
+		sector{ activator->sector },
+		po{ activator->po },
+		fromLineSpecial{ static_cast<bool>(activator->fromLineSpecial) }
+	{
+		P_SetTarget(&mo, activator->mo);
+	}
+
+	~ThreadInfo()
+	{
+		if (thread_era == thinker_era)
+		{
+			P_SetTarget(&mo, nullptr);
+		}
+	}
+
+	ThreadInfo &operator = (const ThreadInfo &info)
+	{
+		thread_era = thinker_era;
+		P_SetTarget(&mo, info.mo);
+		line = info.line;
+		side = info.side;
+		sector = info.sector;
+		po = info.po;
+
+		return *this;
+	}
+};
+
+class Thread : public ACSVM::Thread
+{
+public:
+	ThreadInfo info;
+
+	explicit Thread(ACSVM::Environment *env_) : ACSVM::Thread{env_} {}
+
+	virtual ACSVM::ThreadInfo const *getInfo() const { return &info; }
+
+	virtual void start(
+		ACSVM::Script *script, ACSVM::MapScope *map,
+		const ACSVM::ThreadInfo *info, const ACSVM::Word *argV, ACSVM::Word argC
+	);
+
+	virtual void stop();
+
+	virtual void loadState(ACSVM::Serial &serial);
+
+	virtual void saveState(ACSVM::Serial &serial) const;
+};
+
+}
+
+#endif // __SRB2_ACS_THREAD_HPP__
diff --git a/src/acs/vm/ACSVM/Action.cpp b/src/acs/vm/ACSVM/Action.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..d6aa857ba833c1373239a3f856bef4770feb0648
--- /dev/null
+++ b/src/acs/vm/ACSVM/Action.cpp
@@ -0,0 +1,90 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2015 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// Deferred Action classes.
+//
+//-----------------------------------------------------------------------------
+
+#include "Action.hpp"
+
+#include "Environment.hpp"
+
+
+//----------------------------------------------------------------------------|
+// Extern Functions                                                           |
+//
+
+namespace ACSVM
+{
+   //
+   // ScriptAction move constructor
+   //
+   ScriptAction::ScriptAction(ScriptAction &&a) :
+      action{std::move(a.action)},
+      argV  {std::move(a.argV)},
+      id    {std::move(a.id)},
+      link  {this, std::move(a.link)},
+      name  {std::move(a.name)}
+   {
+   }
+
+   //
+   // ScriptAction constructor
+   //
+   ScriptAction::ScriptAction(ScopeID id_, ScriptName name_, Action action_, Vector<Word> &&argV_) :
+      action{action_},
+      argV  {std::move(argV_)},
+      id    {id_},
+      link  {this},
+      name  {name_}
+   {
+   }
+
+   //
+   // ScriptAction destructor
+   //
+   ScriptAction::~ScriptAction()
+   {
+   }
+
+   //
+   // ScriptAction::lockStrings
+   //
+   void ScriptAction::lockStrings(Environment *env) const
+   {
+      if(name.s) ++name.s->lock;
+
+      for(auto &arg : argV)
+         ++env->getString(arg)->lock;
+   }
+
+   //
+   // ScriptAction::refStrings
+   //
+   void ScriptAction::refStrings(Environment *env) const
+   {
+      if(name.s) name.s->ref = true;
+
+      for(auto &arg : argV)
+         env->getString(arg)->ref = true;
+   }
+
+   //
+   // ScriptAction::unlockStrings
+   //
+   void ScriptAction::unlockStrings(Environment *env) const
+   {
+      if(name.s) --name.s->lock;
+
+      for(auto &arg : argV)
+         --env->getString(arg)->lock;
+   }
+}
+
+// EOF
+
diff --git a/src/acs/vm/ACSVM/Action.hpp b/src/acs/vm/ACSVM/Action.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..a2cef539418fd8e60c7dc8b98155da7b600382cc
--- /dev/null
+++ b/src/acs/vm/ACSVM/Action.hpp
@@ -0,0 +1,83 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2015 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// Deferred Action classes.
+//
+//-----------------------------------------------------------------------------
+
+#ifndef ACSVM__Action_H__
+#define ACSVM__Action_H__
+
+#include "List.hpp"
+#include "Script.hpp"
+#include "Vector.hpp"
+
+
+//----------------------------------------------------------------------------|
+// Types                                                                      |
+//
+
+namespace ACSVM
+{
+   //
+   // ScopeID
+   //
+   class ScopeID
+   {
+   public:
+      ScopeID() = default;
+      ScopeID(Word global_, Word hub_, Word map_) :
+         global{global_}, hub{hub_}, map{map_} {}
+
+      bool operator == (ScopeID const &id) const
+         {return global == id.global && hub == id.hub && map == id.map;}
+      bool operator != (ScopeID const &id) const
+         {return global != id.global || hub != id.hub || map != id.map;}
+
+      Word global;
+      Word hub;
+      Word map;
+   };
+
+   //
+   // ScriptAction
+   //
+   // Represents a deferred Script action.
+   //
+   class ScriptAction
+   {
+   public:
+      enum Action
+      {
+         Start,
+         StartForced,
+         Stop,
+         Pause,
+      };
+
+
+      ScriptAction(ScriptAction &&action);
+      ScriptAction(ScopeID id, ScriptName name, Action action, Vector<Word> &&argV);
+      ~ScriptAction();
+
+      void lockStrings(Environment *env) const;
+
+      void refStrings(Environment *env) const;
+
+      void unlockStrings(Environment *env) const;
+
+      Action                 action;
+      Vector<Word>           argV;
+      ScopeID                id;
+      ListLink<ScriptAction> link;
+      ScriptName             name;
+   };
+}
+
+#endif//ACSVM__Action_H__
+
diff --git a/src/acs/vm/ACSVM/Array.cpp b/src/acs/vm/ACSVM/Array.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..510f847269482145f2d12f9d6107a83a20687196
--- /dev/null
+++ b/src/acs/vm/ACSVM/Array.cpp
@@ -0,0 +1,214 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2015-2017 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// Array class.
+//
+//-----------------------------------------------------------------------------
+
+#include "Array.hpp"
+
+#include "BinaryIO.hpp"
+#include "Environment.hpp"
+#include "Serial.hpp"
+
+
+//----------------------------------------------------------------------------|
+// Static Functions                                                           |
+//
+
+namespace ACSVM
+{
+   //
+   // FreeData (Word)
+   //
+   static void FreeData(Word &)
+   {
+   }
+
+   //
+   // FreeData
+   //
+   template<typename T>
+   static void FreeData(T *&data)
+   {
+      if(!data) return;
+
+      for(auto &itr : *data)
+         FreeData(itr);
+
+      delete[] data;
+      data = nullptr;
+   }
+
+   //
+   // ReadData (Word)
+   //
+   static void ReadData(std::istream &in, Word &out)
+   {
+      out = ReadVLN<Word>(in);
+   }
+
+   //
+   // ReadData
+   //
+   template<typename T>
+   static void ReadData(std::istream &in, T *&out)
+   {
+      if(in.get())
+      {
+         if(!out) out = new T[1]{};
+
+         for(auto &itr : *out)
+            ReadData(in, itr);
+      }
+      else
+         FreeData(out);
+   }
+
+   //
+   // RefStringsData (Word)
+   //
+   static void RefStringsData(Environment *env, Word const &data, void (*ref)(String *))
+   {
+      ref(env->getString(data));
+   }
+
+   //
+   // RefStringsData
+   //
+   template<typename T>
+   static void RefStringsData(Environment *env, T *data, void (*ref)(String *))
+   {
+      if(data) for(auto &itr : *data)
+         RefStringsData(env, itr, ref);
+   }
+
+   //
+   // WriteData (Word)
+   //
+   static void WriteData(std::ostream &out, Word const &in)
+   {
+      WriteVLN(out, in);
+   }
+
+   //
+   // WriteData
+   //
+   template<typename T>
+   static void WriteData(std::ostream &out, T *const &in)
+   {
+      if(in)
+      {
+         out.put('\1');
+
+         for(auto &itr : *in)
+            WriteData(out, itr);
+      }
+      else
+         out.put('\0');
+   }
+}
+
+
+//----------------------------------------------------------------------------|
+// Extern Functions                                                           |
+//
+
+namespace ACSVM
+{
+   //
+   // Array::operator [Word]
+   //
+   Word &Array::operator [] (Word idx)
+   {
+      if(!data) data = new Data[1]{};
+      Bank *&bank = (*data)[idx / (BankSize * SegmSize * PageSize)];
+
+      if(!bank) bank = new Bank[1]{};
+      Segm *&segm = (*bank)[idx / (SegmSize * PageSize) % BankSize];
+
+      if(!segm) segm = new Segm[1]{};
+      Page *&page = (*segm)[idx / PageSize % SegmSize];
+
+      if(!page) page = new Page[1]{};
+      return (*page)[idx % PageSize];
+   }
+
+   //
+   // Array::find
+   //
+   Word Array::find(Word idx) const
+   {
+      if(!data) return 0;
+      Bank *&bank = (*data)[idx / (BankSize * SegmSize * PageSize)];
+
+      if(!bank) return 0;
+      Segm *&segm = (*bank)[idx / (SegmSize * PageSize) % BankSize];
+
+      if(!segm) return 0;
+      Page *&page = (*segm)[idx / PageSize % SegmSize];
+
+      if(!page) return 0;
+      return (*page)[idx % PageSize];
+   }
+
+   //
+   // Array::clear
+   //
+   void Array::clear()
+   {
+      FreeData(data);
+   }
+
+   //
+   // Array::loadState
+   //
+   void Array::loadState(Serial &in)
+   {
+      in.readSign(Signature::Array);
+      ReadData(in, data);
+      in.readSign(~Signature::Array);
+   }
+
+   //
+   // Array::lockStrings
+   //
+   void Array::lockStrings(Environment *env) const
+   {
+      RefStringsData(env, data, [](String *s){++s->lock;});
+   }
+
+   //
+   // Array::refStrings
+   //
+   void Array::refStrings(Environment *env) const
+   {
+      RefStringsData(env, data, [](String *s){s->ref = true;});
+   }
+
+   //
+   // Array::saveState
+   //
+   void Array::saveState(Serial &out) const
+   {
+      out.writeSign(Signature::Array);
+      WriteData(out, data);
+      out.writeSign(~Signature::Array);
+   }
+
+   //
+   // Array::unlockStrings
+   //
+   void Array::unlockStrings(Environment *env) const
+   {
+      RefStringsData(env, data, [](String *s){--s->lock;});
+   }
+}
+
+// EOF
+
diff --git a/src/acs/vm/ACSVM/Array.hpp b/src/acs/vm/ACSVM/Array.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..a71f3b58a0d79418a5f24db08fdf36a4808d6fae
--- /dev/null
+++ b/src/acs/vm/ACSVM/Array.hpp
@@ -0,0 +1,71 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2015-2017 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// Array class.
+//
+//-----------------------------------------------------------------------------
+
+#ifndef ACSVM__Array_H__
+#define ACSVM__Array_H__
+
+#include "Types.hpp"
+
+
+//----------------------------------------------------------------------------|
+// Types                                                                      |
+//
+
+namespace ACSVM
+{
+   //
+   // Array
+   //
+   // Sparse-allocation array of 2**32 Words.
+   //
+   class Array
+   {
+   public:
+      Array() : data{nullptr} {}
+      Array(Array const &) = delete;
+      Array(Array &&array) : data{array.data} {array.data = nullptr;}
+      ~Array() {clear();}
+
+      Word &operator [] (Word idx);
+
+      void clear();
+
+      // If idx is allocated, returns that Word. Otherwise, returns 0.
+      Word find(Word idx) const;
+
+      void loadState(Serial &in);
+
+      void lockStrings(Environment *env) const;
+
+      void refStrings(Environment *env) const;
+
+      void saveState(Serial &out) const;
+
+      void unlockStrings(Environment *env) const;
+
+   private:
+      static constexpr std::size_t PageSize = 256;
+      static constexpr std::size_t SegmSize = 256;
+      static constexpr std::size_t BankSize = 256;
+      static constexpr std::size_t DataSize = 256;
+
+      using Page = Word [PageSize];
+      using Segm = Page*[SegmSize];
+      using Bank = Segm*[BankSize];
+      using Data = Bank*[DataSize];
+
+      Data *data;
+   };
+}
+
+#endif//ACSVM__Array_H__
+
diff --git a/src/acs/vm/ACSVM/BinaryIO.cpp b/src/acs/vm/ACSVM/BinaryIO.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..be229bad7550b1f13a30623421ced532412068c0
--- /dev/null
+++ b/src/acs/vm/ACSVM/BinaryIO.cpp
@@ -0,0 +1,44 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2017 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// Binary data reading/writing primitives.
+//
+//-----------------------------------------------------------------------------
+
+#include "BinaryIO.hpp"
+
+
+//----------------------------------------------------------------------------|
+// Extern Functions                                                           |
+//
+
+namespace ACSVM
+{
+   //
+   // ReadLE4
+   //
+   std::uint_fast32_t ReadLE4(std::istream &in)
+   {
+      Byte buf[4];
+      for(auto &b : buf) b = in.get();
+      return ReadLE4(buf);
+   }
+
+   //
+   // WriteLE4
+   //
+   void WriteLE4(std::ostream &out, std::uint_fast32_t in)
+   {
+      Byte buf[4];
+      WriteLE4(buf, in);
+      for(auto b : buf) out.put(b);
+   }
+}
+
+// EOF
+
diff --git a/src/acs/vm/ACSVM/BinaryIO.hpp b/src/acs/vm/ACSVM/BinaryIO.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..56aeea84b07d91c2b793733095c3748d466b859d
--- /dev/null
+++ b/src/acs/vm/ACSVM/BinaryIO.hpp
@@ -0,0 +1,118 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2015-2017 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// Binary data reading/writing primitives.
+//
+//-----------------------------------------------------------------------------
+
+#ifndef ACSVM__BinaryIO_H__
+#define ACSVM__BinaryIO_H__
+
+#include "Types.hpp"
+
+#include <istream>
+#include <ostream>
+#include <climits>
+
+
+//----------------------------------------------------------------------------|
+// Extern Functions                                                           |
+//
+
+namespace ACSVM
+{
+   std::uint_fast8_t  ReadLE1(Byte const *data);
+   std::uint_fast16_t ReadLE2(Byte const *data);
+   std::uint_fast32_t ReadLE4(Byte const *data);
+   std::uint_fast32_t ReadLE4(std::istream &in);
+
+   template<typename T>
+   T ReadVLN(std::istream &in);
+
+   void WriteLE4(Byte *out, std::uint_fast32_t in);
+   void WriteLE4(std::ostream &out, std::uint_fast32_t in);
+
+   template<typename T>
+   void WriteVLN(std::ostream &out, T in);
+
+   //
+   // ReadLE1
+   //
+   inline std::uint_fast8_t ReadLE1(Byte const *data)
+   {
+      return static_cast<std::uint_fast8_t>(data[0]);
+   }
+
+   //
+   // ReadLE2
+   //
+   inline std::uint_fast16_t ReadLE2(Byte const *data)
+   {
+      return
+         (static_cast<std::uint_fast16_t>(data[0]) << 0) |
+         (static_cast<std::uint_fast16_t>(data[1]) << 8);
+   }
+
+   //
+   // ReadLE4
+   //
+   inline std::uint_fast32_t ReadLE4(Byte const *data)
+   {
+      return
+         (static_cast<std::uint_fast32_t>(data[0]) <<  0) |
+         (static_cast<std::uint_fast32_t>(data[1]) <<  8) |
+         (static_cast<std::uint_fast32_t>(data[2]) << 16) |
+         (static_cast<std::uint_fast32_t>(data[3]) << 24);
+   }
+
+   //
+   // ReadVLN
+   //
+   template<typename T>
+   T ReadVLN(std::istream &in)
+   {
+      T out{0};
+
+      unsigned char c;
+      while(((c = in.get()) & 0x80) && in)
+         out = (out << 7) + (c & 0x7F);
+      out = (out << 7) + c;
+
+      return out;
+   }
+
+   //
+   // WriteLE4
+   //
+   inline void WriteLE4(Byte *out, std::uint_fast32_t in)
+   {
+      out[0] = (in >>  0) & 0xFF;
+      out[1] = (in >>  8) & 0xFF;
+      out[2] = (in >> 16) & 0xFF;
+      out[3] = (in >> 24) & 0xFF;
+   }
+
+   //
+   // WriteVLN
+   //
+   template<typename T>
+   void WriteVLN(std::ostream &out, T in)
+   {
+      constexpr std::size_t len = (sizeof(T) * CHAR_BIT + 6) / 7;
+      char buf[len], *ptr = buf + len;
+
+      *--ptr = static_cast<char>(in & 0x7F);
+      while((in >>= 7))
+         *--ptr = static_cast<char>(in & 0x7F) | 0x80;
+
+      out.write(ptr, (buf + len) - ptr);
+   }
+}
+
+#endif//ACSVM__BinaryIO_H__
+
diff --git a/src/acs/vm/ACSVM/CMakeLists.txt b/src/acs/vm/ACSVM/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..29760733fb97ed9ff32d6a74f6c947f1a8500678
--- /dev/null
+++ b/src/acs/vm/ACSVM/CMakeLists.txt
@@ -0,0 +1,82 @@
+##-----------------------------------------------------------------------------
+##
+## Copyright (C) 2015-2017 David Hill
+##
+## See COPYING for license information.
+##
+##-----------------------------------------------------------------------------
+##
+## CMake file for acsvm.
+##
+##-----------------------------------------------------------------------------
+
+
+##----------------------------------------------------------------------------|
+## Environment Configuration                                                  |
+##
+
+include_directories(.)
+
+
+##----------------------------------------------------------------------------|
+## Targets                                                                    |
+##
+
+##
+## acsvm
+##
+add_library(acsvm ${ACSVM_SHARED_DECL}
+   Action.cpp
+   Action.hpp
+   Array.cpp
+   Array.hpp
+   BinaryIO.cpp
+   BinaryIO.hpp
+   CallFunc.cpp
+   CallFunc.hpp
+   Code.hpp
+   CodeData.cpp
+   CodeData.hpp
+   CodeList.hpp
+   Environment.cpp
+   Environment.hpp
+   Error.cpp
+   Error.hpp
+   Function.cpp
+   Function.hpp
+   HashMap.hpp
+   HashMapFixed.hpp
+   ID.hpp
+   Init.cpp
+   Init.hpp
+   Jump.cpp
+   Jump.hpp
+   Module.cpp
+   Module.hpp
+   ModuleACS0.cpp
+   ModuleACSE.cpp
+   PrintBuf.cpp
+   PrintBuf.hpp
+   Scope.cpp
+   Scope.hpp
+   Script.cpp
+   Script.hpp
+   Serial.cpp
+   Serial.hpp
+   Stack.hpp
+   Store.hpp
+   String.cpp
+   String.hpp
+   Thread.cpp
+   Thread.hpp
+   ThreadExec.cpp
+   Tracer.cpp
+   Tracer.hpp
+   Types.hpp
+   Vector.hpp
+)
+
+ACSVM_INSTALL_LIB(acsvm)
+
+## EOF
+
diff --git a/src/acs/vm/ACSVM/CallFunc.cpp b/src/acs/vm/ACSVM/CallFunc.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..142865e2da71c62b5f8c86985e20cc4e398ea288
--- /dev/null
+++ b/src/acs/vm/ACSVM/CallFunc.cpp
@@ -0,0 +1,480 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2015 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// Internal CallFunc functions.
+//
+//-----------------------------------------------------------------------------
+
+#include "CallFunc.hpp"
+
+#include "Action.hpp"
+#include "Code.hpp"
+#include "Environment.hpp"
+#include "Module.hpp"
+#include "Scope.hpp"
+#include "Thread.hpp"
+
+#include <cctype>
+#include <cinttypes>
+
+
+//----------------------------------------------------------------------------|
+// Static Functions                                                           |
+//
+
+namespace ACSVM
+{
+   //
+   // PrintArray
+   //
+   static void PrintArray(Thread *thread, Word const *argv, Word argc, Array const &arr)
+   {
+      Word idx = argv[0] + (argc > 2 ? argv[2] : 0);
+      Word len = argc > 3 ? argv[3] : -1;
+
+      thread->env->printArray(thread->printBuf, arr, idx, len);
+   }
+
+   //
+   // StrCaseCmp
+   //
+   static int StrCaseCmp(String *l, String *r, Word n)
+   {
+      for(char const *ls = l->str, *rs = r->str;; ++ls, ++rs)
+      {
+         char lc = std::toupper(*ls), rc = std::toupper(*rs);
+         if(lc != rc) return lc < rc ? -1 : 1;
+         if(!lc || !n--) return 0;
+      }
+   }
+
+   //
+   // StrCmp
+   //
+   static int StrCmp(String *l, String *r, Word n)
+   {
+      for(char const *ls = l->str, *rs = r->str;; ++ls, ++rs)
+      {
+         char lc = *ls, rc = *rs;
+         if(lc != rc) return lc < rc ? -1 : 1;
+         if(!lc || !n--) return 0;
+      }
+   }
+
+   //
+   // StrCpyArray
+   //
+   static bool StrCpyArray(Thread *thread, Word const *argv, Array &dst)
+   {
+      Word    dstOff = argv[0] + argv[2];
+      Word    dstLen = argv[3];
+      String *src = thread->scopeMap->getString(argv[4]);
+      Word    srcIdx = argv[5];
+
+      if(srcIdx > src->len) return false;
+
+      for(Word dstIdx = dstOff;;)
+      {
+         if(dstIdx - dstOff == dstLen) return false;
+         if(!(dst[dstIdx++] = src->str[srcIdx++])) return true;
+      }
+   }
+}
+
+
+//----------------------------------------------------------------------------|
+// Extern Functions                                                           |
+//
+
+namespace ACSVM
+{
+   //
+   // void Nop()
+   //
+   bool CallFunc_Func_Nop(Thread *, Word const *, Word)
+   {
+      return false;
+   }
+
+   //
+   // [[noreturn]] void Kill()
+   //
+   bool CallFunc_Func_Kill(Thread *thread, Word const *, Word)
+   {
+      thread->env->printKill(thread, static_cast<Word>(KillType::UnknownFunc), 0);
+      thread->state = ThreadState::Stopped;
+      return true;
+   }
+
+   //======================================================
+   // Printing Functions
+   //
+
+   //
+   // void PrintChar(char c)
+   //
+   bool CallFunc_Func_PrintChar(Thread *thread, Word const *argv, Word)
+   {
+      thread->printBuf.reserve(1);
+      thread->printBuf.put(static_cast<char>(argv[0]));
+      return false;
+   }
+
+   //
+   // str PrintEndStr()
+   //
+   bool CallFunc_Func_PrintEndStr(Thread *thread, Word const *, Word)
+   {
+      char const *data = thread->printBuf.data();
+      std::size_t size = thread->printBuf.size();
+      String     *str  = thread->env->getString(data, size);
+      thread->printBuf.drop();
+      thread->dataStk.push(~str->idx);
+      return false;
+   }
+
+   //
+   // void PrintFixD(fixed d)
+   //
+   bool CallFunc_Func_PrintFixD(Thread *thread, Word const *argv, Word)
+   {
+      // %E worst case: -3.276800e+04 == 13
+      // %F worst case: -32767.999985 == 13
+      // %G worst case: -1.52588e-05  == 12
+      // %G should be maximally P+6 + extra exponent digits.
+      thread->printBuf.reserve(12);
+      thread->printBuf.format("%G", static_cast<std::int32_t>(argv[0]) / 65536.0);
+      return false;
+   }
+
+   //
+   // void PrintGblArr(int idx, int arr, int off = 0, int len = -1)
+   //
+   bool CallFunc_Func_PrintGblArr(Thread *thread, Word const *argv, Word argc)
+   {
+      PrintArray(thread, argv, argc, thread->scopeGbl->arrV[argv[1]]);
+      return false;
+   }
+
+   //
+   // void PrintHubArr(int idx, int arr, int off = 0, int len = -1)
+   //
+   bool CallFunc_Func_PrintHubArr(Thread *thread, Word const *argv, Word argc)
+   {
+      PrintArray(thread, argv, argc, thread->scopeHub->arrV[argv[1]]);
+      return false;
+   }
+
+   //
+   // void PrintIntB(int b)
+   //
+   bool CallFunc_Func_PrintIntB(Thread *thread, Word const *argv, Word)
+   {
+      // %B worst case: 11111111111111111111111111111111 == 32
+      char buf[32], *end = buf+32, *itr = end;
+      for(Word b = argv[0]; b; b >>= 1) *--itr = '0' + (b & 1);
+      thread->printBuf.reserve(end - itr);
+      thread->printBuf.put(itr, end - itr);
+      return false;
+   }
+
+   //
+   // void PrintIntD(int d)
+   //
+   bool CallFunc_Func_PrintIntD(Thread *thread, Word const *argv, Word)
+   {
+      // %d worst case: -2147483648 == 11
+      thread->printBuf.reserve(11);
+      thread->printBuf.format("%" PRId32, static_cast<std::int32_t>(argv[0]));
+      return false;
+   }
+
+   //
+   // void PrintIntX(int x)
+   //
+   bool CallFunc_Func_PrintIntX(Thread *thread, Word const *argv, Word)
+   {
+      // %d worst case: FFFFFFFF == 8
+      thread->printBuf.reserve(8);
+      thread->printBuf.format("%" PRIX32, static_cast<std::uint32_t>(argv[0]));
+      return false;
+   }
+
+   //
+   // void PrintLocArr(int idx, int arr, int off = 0, int len = -1)
+   //
+   bool CallFunc_Func_PrintLocArr(Thread *thread, Word const *argv, Word argc)
+   {
+      PrintArray(thread, argv, argc, thread->localArr[argv[1]]);
+      return false;
+   }
+
+   //
+   // void PrintModArr(int idx, int arr, int off = 0, int len = -1)
+   //
+   bool CallFunc_Func_PrintModArr(Thread *thread, Word const *argv, Word argc)
+   {
+      PrintArray(thread, argv, argc, *thread->scopeMod->arrV[argv[1]]);
+      return false;
+   }
+
+   //
+   // void PrintPush()
+   //
+   bool CallFunc_Func_PrintPush(Thread *thread, Word const *, Word)
+   {
+      thread->printBuf.push();
+      return false;
+   }
+
+   //
+   // void PrintString(str s)
+   //
+   bool CallFunc_Func_PrintString(Thread *thread, Word const *argv, Word)
+   {
+      String *s = thread->scopeMap->getString(argv[0]);
+      thread->printBuf.reserve(s->len0);
+      thread->printBuf.put(s->str, s->len0);
+      return false;
+   }
+
+   //======================================================
+   // Script Functions
+   //
+
+   //
+   // int ScrPauseS(str name, int map)
+   //
+   bool CallFunc_Func_ScrPauseS(Thread *thread, Word const *argV, Word)
+   {
+      String *name = thread->scopeMap->getString(argV[0]);
+      ScopeID scope{thread->scopeGbl->id, thread->scopeHub->id, argV[1]};
+      if(!scope.map) scope.map = thread->scopeMap->id;
+
+      thread->dataStk.push(thread->scopeMap->scriptPause(name, scope));
+      return false;
+   }
+
+   //
+   // int ScrStartS(str name, int map, ...)
+   //
+   bool CallFunc_Func_ScrStartS(Thread *thread, Word const *argV, Word argC)
+   {
+      String *name = thread->scopeMap->getString(argV[0]);
+      ScopeID scope{thread->scopeGbl->id, thread->scopeHub->id, argV[1]};
+      if(!scope.map) scope.map = thread->scopeMap->id;
+
+      thread->dataStk.push(thread->scopeMap->scriptStart(name, scope, {argV+2, argC-2}));
+      return false;
+   }
+
+   //
+   // int ScrStartSD(str name, int map, int arg0, int arg1, int lock)
+   //
+   bool CallFunc_Func_ScrStartSD(Thread *thread, Word const *argV, Word)
+   {
+      if(!thread->env->checkLock(thread, argV[4], true))
+      {
+         thread->dataStk.push(0);
+         return false;
+      }
+
+      return CallFunc_Func_ScrStartS(thread, argV, 4);
+   }
+
+   //
+   // int ScrStartSF(str name, int map, ...)
+   //
+   bool CallFunc_Func_ScrStartSF(Thread *thread, Word const *argV, Word argC)
+   {
+      String *name = thread->scopeMap->getString(argV[0]);
+      ScopeID scope{thread->scopeGbl->id, thread->scopeHub->id, argV[1]};
+      if(!scope.map) scope.map = thread->scopeMap->id;
+
+      thread->dataStk.push(thread->scopeMap->scriptStartForced(name, scope, {argV+2, argC-2}));
+      return false;
+   }
+
+   //
+   // int ScrStartSL(str name, int map, int arg0, int arg1, int lock)
+   //
+   bool CallFunc_Func_ScrStartSL(Thread *thread, Word const *argV, Word)
+   {
+      if(!thread->env->checkLock(thread, argV[4], false))
+      {
+         thread->dataStk.push(0);
+         return false;
+      }
+
+      return CallFunc_Func_ScrStartS(thread, argV, 4);
+   }
+
+   //
+   // int ScrStartSR(str name, ...)
+   //
+   bool CallFunc_Func_ScrStartSR(Thread *thread, Word const *argV, Word argC)
+   {
+      String *name = thread->scopeMap->getString(argV[0]);
+
+      thread->dataStk.push(thread->scopeMap->scriptStartResult(name, {argV+1, argC-1}));
+      return false;
+   }
+
+   //
+   // int ScrStopS(str name, int map)
+   //
+   bool CallFunc_Func_ScrStopS(Thread *thread, Word const *argV, Word)
+   {
+      String *name = thread->scopeMap->getString(argV[0]);
+      ScopeID scope{thread->scopeGbl->id, thread->scopeHub->id, argV[1]};
+      if(!scope.map) scope.map = thread->scopeMap->id;
+
+      thread->dataStk.push(thread->scopeMap->scriptStop(name, scope));
+      return false;
+   }
+
+   //======================================================
+   // String Functions
+   //
+
+   //
+   // int GetChar(str s, int i)
+   //
+   bool CallFunc_Func_GetChar(Thread *thread, Word const *argv, Word)
+   {
+      thread->dataStk.push(thread->scopeMap->getString(argv[0])->get(argv[1]));
+      return false;
+   }
+
+   //
+   // int StrCaseCmp(str l, str r, int n = -1)
+   //
+   bool CallFunc_Func_StrCaseCmp(Thread *thread, Word const *argv, Word argc)
+   {
+      String *l = thread->scopeMap->getString(argv[0]);
+      String *r = thread->scopeMap->getString(argv[1]);
+      Word    n = argc > 2 ? argv[2] : -1;
+
+      thread->dataStk.push(StrCaseCmp(l, r, n));
+      return false;
+   }
+
+   //
+   // int StrCmp(str l, str r, int n = -1)
+   //
+   bool CallFunc_Func_StrCmp(Thread *thread, Word const *argv, Word argc)
+   {
+      String *l = thread->scopeMap->getString(argv[0]);
+      String *r = thread->scopeMap->getString(argv[1]);
+      Word    n = argc > 2 ? argv[2] : -1;
+
+      thread->dataStk.push(StrCmp(l, r, n));
+      return false;
+   }
+
+   //
+   // int StrCpyGblArr(int idx, int dst, int dstOff, int dstLen, str src, int srcOff)
+   //
+   bool CallFunc_Func_StrCpyGblArr(Thread *thread, Word const *argv, Word)
+   {
+      thread->dataStk.push(StrCpyArray(thread, argv, thread->scopeGbl->arrV[argv[1]]));
+      return false;
+   }
+
+   //
+   // int StrCpyHubArr(int idx, int dst, int dstOff, int dstLen, str src, int srcOff)
+   //
+   bool CallFunc_Func_StrCpyHubArr(Thread *thread, Word const *argv, Word)
+   {
+      thread->dataStk.push(StrCpyArray(thread, argv, thread->scopeHub->arrV[argv[1]]));
+      return false;
+   }
+
+   //
+   // int StrCpyLocArr(int idx, int dst, int dstOff, int dstLen, str src, int srcOff)
+   //
+   bool CallFunc_Func_StrCpyLocArr(Thread *thread, Word const *argv, Word)
+   {
+      thread->dataStk.push(StrCpyArray(thread, argv, thread->localArr[argv[1]]));
+      return false;
+   }
+
+   //
+   // int StrCpyModArr(int idx, int dst, int dstOff, int dstLen, str src, int srcOff)
+   //
+   bool CallFunc_Func_StrCpyModArr(Thread *thread, Word const *argv, Word)
+   {
+      thread->dataStk.push(StrCpyArray(thread, argv, *thread->scopeMod->arrV[argv[1]]));
+      return false;
+   }
+
+   //
+   // str StrLeft(str s, int len)
+   //
+   bool CallFunc_Func_StrLeft(Thread *thread, Word const *argv, Word)
+   {
+      String *str = thread->scopeMap->getString(argv[0]);
+      Word    len = argv[1];
+
+      if(len < str->len)
+         str = thread->env->getString(str->str, len);
+
+      thread->dataStk.push(~str->idx);
+      return false;
+   }
+
+   //
+   // int StrLen(str s)
+   //
+   bool CallFunc_Func_StrLen(Thread *thread, Word const *argv, Word)
+   {
+      thread->dataStk.push(thread->scopeMap->getString(argv[0])->len0);
+      return false;
+   }
+
+   //
+   // str StrMid(str s, int idx, int len)
+   //
+   bool CallFunc_Func_StrMid(Thread *thread, Word const *argv, Word)
+   {
+      String *str = thread->scopeMap->getString(argv[0]);
+      Word    idx = argv[1];
+      Word    len = argv[2];
+
+      if(idx < str->len)
+      {
+         if(len < str->len - idx)
+            str = thread->env->getString(str->str + idx, len);
+         else
+            str = thread->env->getString(str->str + idx, str->len - idx);
+      }
+      else
+         str = thread->env->getString("", static_cast<std::size_t>(0));
+
+      thread->dataStk.push(~str->idx);
+      return false;
+   }
+
+   //
+   // str StrRight(str s, int len)
+   //
+   bool CallFunc_Func_StrRight(Thread *thread, Word const *argv, Word)
+   {
+      String *str = thread->scopeMap->getString(argv[0]);
+      Word    len = argv[1];
+
+      if(len < str->len)
+         str = thread->env->getString(str->str + str->len - len, len);
+
+      thread->dataStk.push(~str->idx);
+      return false;
+   }
+}
+
+// EOF
+
diff --git a/src/acs/vm/ACSVM/CallFunc.hpp b/src/acs/vm/ACSVM/CallFunc.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..8813e85462a93546ae1f327d77df1b4e528160d4
--- /dev/null
+++ b/src/acs/vm/ACSVM/CallFunc.hpp
@@ -0,0 +1,31 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2015 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// Internal CallFunc functions.
+//
+//-----------------------------------------------------------------------------
+
+#ifndef ACSVM__CallFunc_H__
+#define ACSVM__CallFunc_H__
+
+#include "Types.hpp"
+
+
+//----------------------------------------------------------------------------|
+// Extern Functions                                                           |
+//
+
+namespace ACSVM
+{
+   #define ACSVM_FuncList(name) \
+      bool (CallFunc_Func_##name)(Thread *thread, Word const *argv, Word argc);
+   #include "CodeList.hpp"
+}
+
+#endif//ACSVM__CallFunc_H__
+
diff --git a/src/acs/vm/ACSVM/Code.hpp b/src/acs/vm/ACSVM/Code.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..a3b16cc7617eda1f06d8fbf272046bf7d1852082
--- /dev/null
+++ b/src/acs/vm/ACSVM/Code.hpp
@@ -0,0 +1,91 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2015 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// Code classes.
+//
+//-----------------------------------------------------------------------------
+
+#ifndef ACSVM__Code_H__
+#define ACSVM__Code_H__
+
+#include "Types.hpp"
+
+
+//----------------------------------------------------------------------------|
+// Types                                                                      |
+//
+
+namespace ACSVM
+{
+   //
+   // Code
+   //
+   // Internal codes.
+   //
+   enum class Code
+   {
+      #define ACSVM_CodeList(name, ...) name,
+      #include "CodeList.hpp"
+
+      None
+   };
+
+   //
+   // CodeACS0
+   //
+   // ACS0 codes.
+   //
+   enum class CodeACS0
+   {
+      #define ACSVM_CodeListACS0(name, idx, ...) name = idx,
+      #include "CodeList.hpp"
+
+      None
+   };
+
+   //
+   // Func
+   //
+   // Internal CallFunc indexes.
+   //
+   enum class Func
+   {
+      #define ACSVM_FuncList(name) name,
+      #include "CodeList.hpp"
+
+      None
+   };
+
+   //
+   // FuncACS0
+   //
+   // ACS0 CallFunc indexes.
+   //
+   enum class FuncACS0
+   {
+      #define ACSVM_FuncListACS0(name, idx, ...) name = idx,
+      #include "CodeList.hpp"
+
+      None
+   };
+
+   //
+   // KillType
+   //
+   enum class KillType
+   {
+      None,
+      OutOfBounds,
+      UnknownCode,
+      UnknownFunc,
+      BranchLimit,
+   };
+}
+
+#endif//ACSVM__Code_H__
+
diff --git a/src/acs/vm/ACSVM/CodeData.cpp b/src/acs/vm/ACSVM/CodeData.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..5a05ed4ae07b657374cada59e5f93feb18204a42
--- /dev/null
+++ b/src/acs/vm/ACSVM/CodeData.cpp
@@ -0,0 +1,205 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2015 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// CodeData classes.
+//
+//-----------------------------------------------------------------------------
+
+#include "CodeData.hpp"
+
+#include "Code.hpp"
+
+#include <algorithm>
+#include <cstring>
+
+
+//----------------------------------------------------------------------------|
+// Extern Functions                                                           |
+//
+
+namespace ACSVM
+{
+   //
+   // CodeDataACS0 constructor
+   //
+   CodeDataACS0::CodeDataACS0(char const *args_, Code transCode_,
+      Word stackArgC_, Word transFunc_) :
+      code     {CodeACS0::None},
+      args     {args_},
+      argc     {CountArgs(args_)},
+      stackArgC{stackArgC_},
+      transCode{transCode_},
+      transFunc{transFunc_}
+   {
+   }
+
+   //
+   // CodeDataACS0 constructor
+   //
+   CodeDataACS0::CodeDataACS0(char const *args_, Word stackArgC_, Word transFunc_) :
+      code     {CodeACS0::None},
+      args     {args_},
+      argc     {CountArgs(args_)},
+      stackArgC{stackArgC_},
+      transCode{argc ? Code::CallFunc_Lit : Code::CallFunc},
+      transFunc{transFunc_}
+   {
+   }
+
+   //
+   // CodeDataACS0 constructor
+   //
+   CodeDataACS0::CodeDataACS0(CodeACS0 code_, char const *args_,
+      Code transCode_, Word stackArgC_, Func transFunc_) :
+      code     {code_},
+      args     {args_},
+      argc     {CountArgs(args_)},
+      stackArgC{stackArgC_},
+      transCode{transCode_},
+      transFunc{transFunc_ != Func::None ? static_cast<Word>(transFunc_) : 0}
+   {
+   }
+
+   //
+   // CodeDataACS0::CountArgs
+   //
+   std::size_t CodeDataACS0::CountArgs(char const *args)
+   {
+      std::size_t argc = 0;
+
+      for(; *args; ++args) switch(*args)
+      {
+      case 'B':
+      case 'H':
+      case 'W':
+      case 'b':
+      case 'h':
+         ++argc;
+         break;
+      }
+
+      return argc;
+   }
+
+   //
+   // FuncDataACS0 copy constructor
+   //
+   FuncDataACS0::FuncDataACS0(FuncDataACS0 const &data) :
+      transFunc{data.transFunc},
+
+      transCodeV{new TransCode[data.transCodeC]},
+      transCodeC{data.transCodeC}
+   {
+      std::copy(data.transCodeV, data.transCodeV + transCodeC, transCodeV);
+   }
+
+   //
+   // FuncDataACS0 move constructor
+   //
+   FuncDataACS0::FuncDataACS0(FuncDataACS0 &&data) :
+      transFunc{data.transFunc},
+
+      transCodeV{data.transCodeV},
+      transCodeC{data.transCodeC}
+   {
+      data.transCodeV = nullptr;
+      data.transCodeC = 0;
+   }
+
+   //
+   // FuncDataACS0 constructor
+   //
+   FuncDataACS0::FuncDataACS0(FuncACS0 func_, Func transFunc_,
+      std::initializer_list<TransCode> transCodes) :
+      func{func_},
+
+      transFunc{transFunc_ != Func::None ? static_cast<Word>(transFunc_) : 0},
+
+      transCodeV{new TransCode[transCodes.size()]},
+      transCodeC{transCodes.size()}
+   {
+      std::copy(transCodes.begin(), transCodes.end(), transCodeV);
+   }
+
+   //
+   // FuncDataACS0 constructor
+   //
+   FuncDataACS0::FuncDataACS0(Word transFunc_) :
+      func{FuncACS0::None},
+
+      transFunc{transFunc_},
+
+      transCodeV{nullptr},
+      transCodeC{0}
+   {
+   }
+
+   //
+   // FuncDataACS0 constructor
+   //
+   FuncDataACS0::FuncDataACS0(Word transFunc_,
+      std::initializer_list<TransCode> transCodes) :
+      func{FuncACS0::None},
+
+      transFunc{transFunc_},
+
+      transCodeV{new TransCode[transCodes.size()]},
+      transCodeC{transCodes.size()}
+   {
+      std::copy(transCodes.begin(), transCodes.end(), transCodeV);
+   }
+
+   //
+   // FuncDataACS0 constructor
+   //
+   FuncDataACS0::FuncDataACS0(Word transFunc_,
+      std::unique_ptr<TransCode[]> &&transCodeV_, std::size_t transCodeC_) :
+      func{FuncACS0::None},
+
+      transFunc{transFunc_},
+
+      transCodeV{transCodeV_.release()},
+      transCodeC{transCodeC_}
+   {
+   }
+
+   //
+   // FuncDataACS0 destructor
+   //
+   FuncDataACS0::~FuncDataACS0()
+   {
+      delete[] transCodeV;
+   }
+
+   //
+   // FuncDataACS0::operator = FuncDataACS0
+   //
+   FuncDataACS0 &FuncDataACS0::operator = (FuncDataACS0 &&data)
+   {
+      std::swap(transFunc, data.transFunc);
+
+      std::swap(transCodeV, data.transCodeV);
+      std::swap(transCodeC, data.transCodeC);
+
+      return *this;
+   }
+
+   //
+   // FuncDataACS0::getTransCode
+   //
+   Code FuncDataACS0::getTransCode(Word argc) const
+   {
+      for(auto itr = transCodeV, end = itr + transCodeC; itr != end; ++itr)
+         if(itr->first == argc) return itr->second;
+
+      return Code::CallFunc;
+   }
+}
+
+// EOF
+
diff --git a/src/acs/vm/ACSVM/CodeData.hpp b/src/acs/vm/ACSVM/CodeData.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..71b6f635afeb61483496229d8a5baaf7c69a07ca
--- /dev/null
+++ b/src/acs/vm/ACSVM/CodeData.hpp
@@ -0,0 +1,132 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2015 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// CodeData classes.
+//
+//-----------------------------------------------------------------------------
+
+#ifndef ACSVM__CodeData_H__
+#define ACSVM__CodeData_H__
+
+#include "Types.hpp"
+
+#include <initializer_list>
+#include <memory>
+#include <utility>
+
+
+//----------------------------------------------------------------------------|
+// Types                                                                      |
+//
+
+namespace ACSVM
+{
+   //
+   // CodeData
+   //
+   // Internal code description.
+   //
+   class CodeData
+   {
+   public:
+      Code code;
+
+      Word argc;
+   };
+
+   //
+   // CodeDataACS0
+   //
+   // ACS0 code description.
+   //
+   class CodeDataACS0
+   {
+   public:
+      CodeDataACS0(char const *args, Code transCode, Word stackArgC,
+         Word transFunc = 0);
+      CodeDataACS0(char const *args, Word stackArgC, Word transFunc);
+      CodeDataACS0(CodeACS0 code, char const *args, Code transCode,
+         Word stackArgC, Func transFunc);
+
+      // Code index. If not an internally recognized code, is set to None.
+      CodeACS0 code;
+
+      // String describing the code's arguments.
+      //    A - Previous value is MapReg index.
+      //    a - Previous Value is MapArr index.
+      //    B - Single byte.
+      //    b - Single byte if compressed, full word otherwise.
+      //    G - Previous value is GblReg index.
+      //    g - Previous Value is GblArr index.
+      //    H - Half word.
+      //    h - Half word if compressed, full word otherwise.
+      //    J - Previous value is jump index.
+      //    L - Previous value is LocReg index.
+      //    l - Previous Value is LocArr index.
+      //    O - Previous value is ModReg index.
+      //    o - Previous Value is ModArr index.
+      //    S - Previous value is string index.
+      //    U - Previous value is HubReg index.
+      //    u - Previous Value is HubArr index.
+      //    W - Full word.
+      char const *args;
+      std::size_t argc;
+
+      // Stack argument count.
+      Word stackArgC;
+
+      // Internal code to translate to.
+      Code transCode;
+
+      // CallFunc index to translate to.
+      Word transFunc;
+
+   private:
+      static std::size_t CountArgs(char const *args);
+   };
+
+   //
+   // FuncDataACS0
+   //
+   // ACS0 CallFunc description.
+   //
+   class FuncDataACS0
+   {
+   public:
+      using TransCode = std::pair<Word, Code>;
+
+      FuncDataACS0(FuncDataACS0 const &);
+      FuncDataACS0(FuncDataACS0 &&data);
+      FuncDataACS0(FuncACS0 func, Func transFunc,
+         std::initializer_list<TransCode> transCodes);
+      FuncDataACS0(Word transFunc);
+      FuncDataACS0(Word transFunc, std::initializer_list<TransCode> transCodes);
+      FuncDataACS0(Word transFunc, std::unique_ptr<TransCode[]> &&transCodeV,
+         std::size_t transCodeC);
+      ~FuncDataACS0();
+
+      FuncDataACS0 &operator = (FuncDataACS0 const &) = delete;
+      FuncDataACS0 &operator = (FuncDataACS0 &&data);
+
+      // Internal code to translate to.
+      Code getTransCode(Word argc) const;
+
+      // CallFunc index. If not internally recognized, is set to None.
+      FuncACS0 func;
+
+      // CallFunc index to translate to.
+      Word transFunc;
+
+   private:
+      TransCode  *transCodeV;
+      std::size_t transCodeC;
+   };
+}
+
+#endif//ACSVM__CodeData_H__
+
diff --git a/src/acs/vm/ACSVM/CodeList.hpp b/src/acs/vm/ACSVM/CodeList.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..7bc3cb009fb2fa9de81ffbf3344e362edf0542f3
--- /dev/null
+++ b/src/acs/vm/ACSVM/CodeList.hpp
@@ -0,0 +1,455 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2015 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// List of all codes.
+//
+//-----------------------------------------------------------------------------
+
+
+#ifdef ACSVM_CodeList
+
+ACSVM_CodeList(Nop,          0)
+ACSVM_CodeList(Kill,         2)
+
+// Binary operator codes.
+#define ACSVM_CodeList_BinaryOpSet(name) \
+   ACSVM_CodeList(name,           0) \
+   ACSVM_CodeList(name##_GblArr,  1) \
+   ACSVM_CodeList(name##_GblReg,  1) \
+   ACSVM_CodeList(name##_HubArr,  1) \
+   ACSVM_CodeList(name##_HubReg,  1) \
+   ACSVM_CodeList(name##_LocArr,  1) \
+   ACSVM_CodeList(name##_LocReg,  1) \
+   ACSVM_CodeList(name##_ModArr,  1) \
+   ACSVM_CodeList(name##_ModReg,  1)
+ACSVM_CodeList_BinaryOpSet(AddU)
+ACSVM_CodeList_BinaryOpSet(AndU)
+ACSVM_CodeList_BinaryOpSet(DivI)
+ACSVM_CodeList_BinaryOpSet(ModI)
+ACSVM_CodeList_BinaryOpSet(MulU)
+ACSVM_CodeList_BinaryOpSet(OrIU)
+ACSVM_CodeList_BinaryOpSet(OrXU)
+ACSVM_CodeList_BinaryOpSet(ShLU)
+ACSVM_CodeList_BinaryOpSet(ShRI)
+ACSVM_CodeList_BinaryOpSet(SubU)
+#undef ACSVM_CodeList_BinaryOpSet
+ACSVM_CodeList(CmpI_GE,      0)
+ACSVM_CodeList(CmpI_GT,      0)
+ACSVM_CodeList(CmpI_LE,      0)
+ACSVM_CodeList(CmpI_LT,      0)
+ACSVM_CodeList(CmpU_EQ,      0)
+ACSVM_CodeList(CmpU_NE,      0)
+ACSVM_CodeList(DivX,         0)
+ACSVM_CodeList(LAnd,         0)
+ACSVM_CodeList(LOrI,         0)
+ACSVM_CodeList(MulX,         0)
+
+// Call codes.
+ACSVM_CodeList(Call_Lit,     1)
+ACSVM_CodeList(Call_Stk,     0)
+ACSVM_CodeList(CallFunc,     2)
+ACSVM_CodeList(CallFunc_Lit, 0)
+ACSVM_CodeList(CallSpec,     2)
+ACSVM_CodeList(CallSpec_Lit, 0)
+ACSVM_CodeList(CallSpec_R1,  2)
+ACSVM_CodeList(Retn,         0)
+
+// Drop codes.
+ACSVM_CodeList(Drop_GblArr,  1)
+ACSVM_CodeList(Drop_GblReg,  1)
+ACSVM_CodeList(Drop_HubArr,  1)
+ACSVM_CodeList(Drop_HubReg,  1)
+ACSVM_CodeList(Drop_LocArr,  1)
+ACSVM_CodeList(Drop_LocReg,  1)
+ACSVM_CodeList(Drop_ModArr,  1)
+ACSVM_CodeList(Drop_ModReg,  1)
+ACSVM_CodeList(Drop_Nul,     0)
+ACSVM_CodeList(Drop_ScrRet,  0)
+
+// Jump codes.
+ACSVM_CodeList(Jcnd_Lit,     2)
+ACSVM_CodeList(Jcnd_Nil,     1)
+ACSVM_CodeList(Jcnd_Tab,     1)
+ACSVM_CodeList(Jcnd_Tru,     1)
+ACSVM_CodeList(Jump_Lit,     1)
+ACSVM_CodeList(Jump_Stk,     0)
+
+// Push codes.
+ACSVM_CodeList(Pfun_Lit,     1)
+ACSVM_CodeList(Pstr_Stk,     0)
+ACSVM_CodeList(Push_GblArr,  1)
+ACSVM_CodeList(Push_GblReg,  1)
+ACSVM_CodeList(Push_HubArr,  1)
+ACSVM_CodeList(Push_HubReg,  1)
+ACSVM_CodeList(Push_Lit,     1)
+ACSVM_CodeList(Push_LitArr,  0)
+ACSVM_CodeList(Push_LocArr,  1)
+ACSVM_CodeList(Push_LocReg,  1)
+ACSVM_CodeList(Push_ModArr,  1)
+ACSVM_CodeList(Push_ModReg,  1)
+ACSVM_CodeList(Push_StrArs,  0)
+
+// Script control codes.
+ACSVM_CodeList(ScrDelay,     0)
+ACSVM_CodeList(ScrDelay_Lit, 1)
+ACSVM_CodeList(ScrHalt,      0)
+ACSVM_CodeList(ScrRestart,   0)
+ACSVM_CodeList(ScrTerm,      0)
+ACSVM_CodeList(ScrWaitI,     0)
+ACSVM_CodeList(ScrWaitI_Lit, 1)
+ACSVM_CodeList(ScrWaitS,     0)
+ACSVM_CodeList(ScrWaitS_Lit, 1)
+
+// Stack control codes.
+ACSVM_CodeList(Copy,         0)
+ACSVM_CodeList(Swap,         0)
+
+// Unary operator codes.
+#define ACSVM_CodeList_UnaryOpSet(name) \
+   ACSVM_CodeList(name##_GblArr,  1) \
+   ACSVM_CodeList(name##_GblReg,  1) \
+   ACSVM_CodeList(name##_HubArr,  1) \
+   ACSVM_CodeList(name##_HubReg,  1) \
+   ACSVM_CodeList(name##_LocArr,  1) \
+   ACSVM_CodeList(name##_LocReg,  1) \
+   ACSVM_CodeList(name##_ModArr,  1) \
+   ACSVM_CodeList(name##_ModReg,  1)
+ACSVM_CodeList_UnaryOpSet(DecU)
+ACSVM_CodeList_UnaryOpSet(IncU)
+#undef ACSVM_CodeList_UnaryOpSet
+ACSVM_CodeList(InvU,         0)
+ACSVM_CodeList(NegI,         0)
+ACSVM_CodeList(NotU,         0)
+
+#undef ACSVM_CodeList
+#endif
+
+
+#ifdef ACSVM_CodeListACS0
+
+ACSVM_CodeListACS0(Nop,            0, "",       Nop,          0, None)
+ACSVM_CodeListACS0(ScrTerm,        1, "",       ScrTerm,      0, None)
+ACSVM_CodeListACS0(ScrHalt,        2, "",       ScrHalt,      0, None)
+ACSVM_CodeListACS0(Push_Lit,       3, "W",      Push_Lit,     0, None)
+ACSVM_CodeListACS0(CallSpec_1,     4, "b",      CallSpec,     1, None)
+ACSVM_CodeListACS0(CallSpec_2,     5, "b",      CallSpec,     2, None)
+ACSVM_CodeListACS0(CallSpec_3,     6, "b",      CallSpec,     3, None)
+ACSVM_CodeListACS0(CallSpec_4,     7, "b",      CallSpec,     4, None)
+ACSVM_CodeListACS0(CallSpec_5,     8, "b",      CallSpec,     5, None)
+ACSVM_CodeListACS0(CallSpec_1L,    9, "bW",     CallSpec_Lit, 0, None)
+ACSVM_CodeListACS0(CallSpec_2L,   10, "bWW",    CallSpec_Lit, 0, None)
+ACSVM_CodeListACS0(CallSpec_3L,   11, "bWWW",   CallSpec_Lit, 0, None)
+ACSVM_CodeListACS0(CallSpec_4L,   12, "bWWWW",  CallSpec_Lit, 0, None)
+ACSVM_CodeListACS0(CallSpec_5L,   13, "bWWWWW", CallSpec_Lit, 0, None)
+ACSVM_CodeListACS0(AddU,          14, "",       AddU,         2, None)
+ACSVM_CodeListACS0(SubU,          15, "",       SubU,         2, None)
+ACSVM_CodeListACS0(MulU,          16, "",       MulU,         2, None)
+ACSVM_CodeListACS0(DivI,          17, "",       DivI,         2, None)
+ACSVM_CodeListACS0(ModI,          18, "",       ModI,         2, None)
+ACSVM_CodeListACS0(CmpU_EQ,       19, "",       CmpU_EQ,      2, None)
+ACSVM_CodeListACS0(CmpU_NE,       20, "",       CmpU_NE,      2, None)
+ACSVM_CodeListACS0(CmpI_LT,       21, "",       CmpI_LT,      2, None)
+ACSVM_CodeListACS0(CmpI_GT,       22, "",       CmpI_GT,      2, None)
+ACSVM_CodeListACS0(CmpI_LE,       23, "",       CmpI_LE,      2, None)
+ACSVM_CodeListACS0(CmpI_GE,       24, "",       CmpI_GE,      2, None)
+ACSVM_CodeListACS0(Drop_LocReg,   25, "bL",     Drop_LocReg,  1, None)
+ACSVM_CodeListACS0(Drop_ModReg,   26, "bO",     Drop_ModReg,  1, None)
+ACSVM_CodeListACS0(Drop_HubReg,   27, "bU",     Drop_HubReg,  1, None)
+ACSVM_CodeListACS0(Push_LocReg,   28, "bL",     Push_LocReg,  1, None)
+ACSVM_CodeListACS0(Push_ModReg,   29, "bO",     Push_ModReg,  1, None)
+ACSVM_CodeListACS0(Push_HubReg,   30, "bU",     Push_HubReg,  1, None)
+ACSVM_CodeListACS0(AddU_LocReg,   31, "bL",     AddU_LocReg,  1, None)
+ACSVM_CodeListACS0(AddU_ModReg,   32, "bO",     AddU_ModReg,  1, None)
+ACSVM_CodeListACS0(AddU_HubReg,   33, "bU",     AddU_HubReg,  1, None)
+ACSVM_CodeListACS0(SubU_LocReg,   34, "bL",     SubU_LocReg,  1, None)
+ACSVM_CodeListACS0(SubU_ModReg,   35, "bO",     SubU_ModReg,  1, None)
+ACSVM_CodeListACS0(SubU_HubReg,   36, "bU",     SubU_HubReg,  1, None)
+ACSVM_CodeListACS0(MulU_LocReg,   37, "bL",     MulU_LocReg,  1, None)
+ACSVM_CodeListACS0(MulU_ModReg,   38, "bO",     MulU_ModReg,  1, None)
+ACSVM_CodeListACS0(MulU_HubReg,   39, "bU",     MulU_HubReg,  1, None)
+ACSVM_CodeListACS0(DivI_LocReg,   40, "bL",     DivI_LocReg,  1, None)
+ACSVM_CodeListACS0(DivI_ModReg,   41, "bO",     DivI_ModReg,  1, None)
+ACSVM_CodeListACS0(DivI_HubReg,   42, "bU",     DivI_HubReg,  1, None)
+ACSVM_CodeListACS0(ModI_LocReg,   43, "bL",     ModI_LocReg,  1, None)
+ACSVM_CodeListACS0(ModI_ModReg,   44, "bO",     ModI_ModReg,  1, None)
+ACSVM_CodeListACS0(ModI_HubReg,   45, "bU",     ModI_HubReg,  1, None)
+ACSVM_CodeListACS0(IncU_LocReg,   46, "bL",     IncU_LocReg,  1, None)
+ACSVM_CodeListACS0(IncU_ModReg,   47, "bO",     IncU_ModReg,  1, None)
+ACSVM_CodeListACS0(IncU_HubReg,   48, "bU",     IncU_HubReg,  1, None)
+ACSVM_CodeListACS0(DecU_LocReg,   49, "bL",     DecU_LocReg,  1, None)
+ACSVM_CodeListACS0(DecU_ModReg,   50, "bO",     DecU_ModReg,  1, None)
+ACSVM_CodeListACS0(DecU_HubReg,   51, "bU",     DecU_HubReg,  1, None)
+ACSVM_CodeListACS0(Jump_Lit,      52, "WJ",     Jump_Lit,     0, None)
+ACSVM_CodeListACS0(Jcnd_Tru,      53, "WJ",     Jcnd_Tru,     1, None)
+ACSVM_CodeListACS0(Drop_Nul,      54, "",       Drop_Nul,     1, None)
+ACSVM_CodeListACS0(ScrDelay,      55, "",       ScrDelay,     1, None)
+ACSVM_CodeListACS0(ScrDelay_Lit,  56, "W",      ScrDelay_Lit, 0, None)
+
+ACSVM_CodeListACS0(ScrRestart,    69, "",       ScrRestart,   0, None)
+ACSVM_CodeListACS0(LAnd,          70, "",       LAnd,         2, None)
+ACSVM_CodeListACS0(LOrI,          71, "",       LOrI,         2, None)
+ACSVM_CodeListACS0(AndU,          72, "",       AndU,         2, None)
+ACSVM_CodeListACS0(OrIU,          73, "",       OrIU,         2, None)
+ACSVM_CodeListACS0(OrXU,          74, "",       OrXU,         2, None)
+ACSVM_CodeListACS0(NotU,          75, "",       NotU,         1, None)
+ACSVM_CodeListACS0(ShLU,          76, "",       ShLU,         2, None)
+ACSVM_CodeListACS0(ShRI,          77, "",       ShRI,         2, None)
+ACSVM_CodeListACS0(NegI,          78, "",       NegI,         1, None)
+ACSVM_CodeListACS0(Jcnd_Nil,      79, "WJ",     Jcnd_Nil,     1, None)
+
+ACSVM_CodeListACS0(ScrWaitI,      81, "",       ScrWaitI,     1, None)
+ACSVM_CodeListACS0(ScrWaitI_Lit,  82, "W",      ScrWaitI_Lit, 0, None)
+
+ACSVM_CodeListACS0(Jcnd_Lit,      84, "WWJ",    Jcnd_Lit,     1, None)
+ACSVM_CodeListACS0(PrintPush,     85, "",       CallFunc,     0, PrintPush)
+
+ACSVM_CodeListACS0(PrintString,   87, "",       CallFunc,     1, PrintString)
+ACSVM_CodeListACS0(PrintIntD,     88, "",       CallFunc,     1, PrintIntD)
+ACSVM_CodeListACS0(PrintChar,     89, "",       CallFunc,     1, PrintChar)
+
+ACSVM_CodeListACS0(MulX,         136, "",       MulX,         2, None)
+ACSVM_CodeListACS0(DivX,         137, "",       DivX,         2, None)
+
+ACSVM_CodeListACS0(PrintFixD,    157, "",       CallFunc,     1, PrintFixD)
+
+ACSVM_CodeListACS0(Push_LitB,    167, "B",      Push_Lit,     0, None)
+ACSVM_CodeListACS0(CallSpec_1LB, 168, "BB",     CallSpec_Lit, 0, None)
+ACSVM_CodeListACS0(CallSpec_2LB, 169, "BBB",    CallSpec_Lit, 0, None)
+ACSVM_CodeListACS0(CallSpec_3LB, 170, "BBBB",   CallSpec_Lit, 0, None)
+ACSVM_CodeListACS0(CallSpec_4LB, 171, "BBBBB",  CallSpec_Lit, 0, None)
+ACSVM_CodeListACS0(CallSpec_5LB, 172, "BBBBBB", CallSpec_Lit, 0, None)
+ACSVM_CodeListACS0(ScrDelay_LB,  173, "B",      ScrDelay_Lit, 0, None)
+ACSVM_CodeListACS0(Push_LitArrB, 175, "",       Push_LitArr,  0, None)
+ACSVM_CodeListACS0(Push_Lit2B,   176, "BB",     Push_LitArr,  0, None)
+ACSVM_CodeListACS0(Push_Lit3B,   177, "BBB",    Push_LitArr,  0, None)
+ACSVM_CodeListACS0(Push_Lit4B,   178, "BBBB",   Push_LitArr,  0, None)
+ACSVM_CodeListACS0(Push_Lit5B,   179, "BBBBB",  Push_LitArr,  0, None)
+
+ACSVM_CodeListACS0(Drop_GblReg,  181, "bG",     Drop_GblReg,  1, None)
+ACSVM_CodeListACS0(Push_GblReg,  182, "bG",     Push_GblReg,  0, None)
+ACSVM_CodeListACS0(AddU_GblReg,  183, "bG",     AddU_GblReg,  1, None)
+ACSVM_CodeListACS0(SubU_GblReg,  184, "bG",     SubU_GblReg,  1, None)
+ACSVM_CodeListACS0(MulU_GblReg,  185, "bG",     MulU_GblReg,  1, None)
+ACSVM_CodeListACS0(DivI_GblReg,  186, "bG",     DivI_GblReg,  1, None)
+ACSVM_CodeListACS0(ModI_GblReg,  187, "bG",     ModI_GblReg,  1, None)
+ACSVM_CodeListACS0(IncU_GblReg,  188, "bG",     IncU_GblReg,  1, None)
+ACSVM_CodeListACS0(DecU_GblReg,  189, "bG",     DecU_GblReg,  1, None)
+
+ACSVM_CodeListACS0(Call_Lit,     203, "b",      Call_Lit,     0, None)
+ACSVM_CodeListACS0(Call_Nul,     204, "b",      Call_Lit,     0, None)
+ACSVM_CodeListACS0(Retn_Nul,     205, "",       Retn,         0, None)
+ACSVM_CodeListACS0(Retn_Stk,     206, "",       Retn,         0, None)
+ACSVM_CodeListACS0(Push_ModArr,  207, "bo",     Push_ModArr,  1, None)
+ACSVM_CodeListACS0(Drop_ModArr,  208, "bo",     Drop_ModArr,  2, None)
+ACSVM_CodeListACS0(AddU_ModArr,  209, "bo",     AddU_ModArr,  2, None)
+ACSVM_CodeListACS0(SubU_ModArr,  210, "bo",     SubU_ModArr,  2, None)
+ACSVM_CodeListACS0(MulU_ModArr,  211, "bo",     MulU_ModArr,  2, None)
+ACSVM_CodeListACS0(DivI_ModArr,  212, "bo",     DivI_ModArr,  2, None)
+ACSVM_CodeListACS0(ModI_ModArr,  213, "bo",     ModI_ModArr,  2, None)
+ACSVM_CodeListACS0(IncU_ModArr,  214, "bo",     IncU_ModArr,  2, None)
+ACSVM_CodeListACS0(DecU_ModArr,  215, "bo",     DecU_ModArr,  2, None)
+ACSVM_CodeListACS0(Copy,         216, "",       Copy,         1, None)
+ACSVM_CodeListACS0(Swap,         217, "",       Swap,         2, None)
+
+ACSVM_CodeListACS0(Pstr_Stk,     225, "",       Pstr_Stk,     1, None)
+ACSVM_CodeListACS0(Push_HubArr,  226, "bu",     Push_HubArr,  1, None)
+ACSVM_CodeListACS0(Drop_HubArr,  227, "bu",     Drop_HubArr,  2, None)
+ACSVM_CodeListACS0(AddU_HubArr,  228, "bu",     AddU_HubArr,  2, None)
+ACSVM_CodeListACS0(SubU_HubArr,  229, "bu",     SubU_HubArr,  2, None)
+ACSVM_CodeListACS0(MulU_HubArr,  230, "bu",     MulU_HubArr,  2, None)
+ACSVM_CodeListACS0(DivI_HubArr,  231, "bu",     DivI_HubArr,  2, None)
+ACSVM_CodeListACS0(ModI_HubArr,  232, "bu",     ModI_HubArr,  2, None)
+ACSVM_CodeListACS0(IncU_HubArr,  233, "bu",     IncU_HubArr,  2, None)
+ACSVM_CodeListACS0(DecU_HubArr,  234, "bu",     DecU_HubArr,  2, None)
+ACSVM_CodeListACS0(Push_GblArr,  235, "bg",     Push_GblArr,  1, None)
+ACSVM_CodeListACS0(Drop_GblArr,  236, "bg",     Drop_GblArr,  2, None)
+ACSVM_CodeListACS0(AddU_GblArr,  237, "bg",     AddU_GblArr,  2, None)
+ACSVM_CodeListACS0(SubU_GblArr,  238, "bg",     SubU_GblArr,  2, None)
+ACSVM_CodeListACS0(MulU_GblArr,  239, "bg",     MulU_GblArr,  2, None)
+ACSVM_CodeListACS0(DivI_GblArr,  240, "bg",     DivI_GblArr,  2, None)
+ACSVM_CodeListACS0(ModI_GblArr,  241, "bg",     ModI_GblArr,  2, None)
+ACSVM_CodeListACS0(IncU_GblArr,  242, "bg",     IncU_GblArr,  2, None)
+ACSVM_CodeListACS0(DecU_GblArr,  243, "bg",     DecU_GblArr,  2, None)
+
+ACSVM_CodeListACS0(StrLen,       253, "",       CallFunc,     1, StrLen)
+
+ACSVM_CodeListACS0(Jcnd_Tab,     256, "",       Jcnd_Tab,     1, None)
+ACSVM_CodeListACS0(Drop_ScrRet,  257, "",       Drop_ScrRet,  1, None)
+
+ACSVM_CodeListACS0(CallSpec_5R1, 263, "b",      CallSpec_R1,  5, None)
+
+ACSVM_CodeListACS0(PrintModArr,  273, "",       CallFunc,     2, PrintModArr)
+ACSVM_CodeListACS0(PrintHubArr,  274, "",       CallFunc,     2, PrintHubArr)
+ACSVM_CodeListACS0(PrintGblArr,  275, "",       CallFunc,     2, PrintGblArr)
+
+ACSVM_CodeListACS0(AndU_LocReg,  291, "bL",     AndU_LocReg,  1, None)
+ACSVM_CodeListACS0(AndU_ModReg,  292, "bO",     AndU_ModReg,  1, None)
+ACSVM_CodeListACS0(AndU_HubReg,  293, "bU",     AndU_HubReg,  1, None)
+ACSVM_CodeListACS0(AndU_GblReg,  294, "bG",     AndU_GblReg,  1, None)
+ACSVM_CodeListACS0(AndU_ModArr,  295, "bo",     AndU_ModArr,  2, None)
+ACSVM_CodeListACS0(AndU_HubArr,  296, "bu",     AndU_HubArr,  2, None)
+ACSVM_CodeListACS0(AndU_GblArr,  297, "bg",     AndU_GblArr,  2, None)
+ACSVM_CodeListACS0(OrXU_LocReg,  298, "bL",     OrXU_LocReg,  1, None)
+ACSVM_CodeListACS0(OrXU_ModReg,  299, "bO",     OrXU_ModReg,  1, None)
+ACSVM_CodeListACS0(OrXU_HubReg,  300, "bU",     OrXU_HubReg,  1, None)
+ACSVM_CodeListACS0(OrXU_GblReg,  301, "bG",     OrXU_GblReg,  1, None)
+ACSVM_CodeListACS0(OrXU_ModArr,  302, "bo",     OrXU_ModArr,  2, None)
+ACSVM_CodeListACS0(OrXU_HubArr,  303, "bu",     OrXU_HubArr,  2, None)
+ACSVM_CodeListACS0(OrXU_GblArr,  304, "bg",     OrXU_GblArr,  2, None)
+ACSVM_CodeListACS0(OrIU_LocReg,  305, "bL",     OrIU_LocReg,  1, None)
+ACSVM_CodeListACS0(OrIU_ModReg,  306, "bO",     OrIU_ModReg,  1, None)
+ACSVM_CodeListACS0(OrIU_HubReg,  307, "bU",     OrIU_HubReg,  1, None)
+ACSVM_CodeListACS0(OrIU_GblReg,  308, "bG",     OrIU_GblReg,  1, None)
+ACSVM_CodeListACS0(OrIU_ModArr,  309, "bo",     OrIU_ModArr,  2, None)
+ACSVM_CodeListACS0(OrIU_HubArr,  310, "bu",     OrIU_HubArr,  2, None)
+ACSVM_CodeListACS0(OrIU_GblArr,  311, "bg",     OrIU_GblArr,  2, None)
+ACSVM_CodeListACS0(ShLU_LocReg,  312, "bL",     ShLU_LocReg,  1, None)
+ACSVM_CodeListACS0(ShLU_ModReg,  313, "bO",     ShLU_ModReg,  1, None)
+ACSVM_CodeListACS0(ShLU_HubReg,  314, "bU",     ShLU_HubReg,  1, None)
+ACSVM_CodeListACS0(ShLU_GblReg,  315, "bG",     ShLU_GblReg,  1, None)
+ACSVM_CodeListACS0(ShLU_ModArr,  316, "bo",     ShLU_ModArr,  2, None)
+ACSVM_CodeListACS0(ShLU_HubArr,  317, "bu",     ShLU_HubArr,  2, None)
+ACSVM_CodeListACS0(ShLU_GblArr,  318, "bg",     ShLU_GblArr,  2, None)
+ACSVM_CodeListACS0(ShRI_LocReg,  319, "bL",     ShRI_LocReg,  1, None)
+ACSVM_CodeListACS0(ShRI_ModReg,  320, "bO",     ShRI_ModReg,  1, None)
+ACSVM_CodeListACS0(ShRI_HubReg,  321, "bU",     ShRI_HubReg,  1, None)
+ACSVM_CodeListACS0(ShRI_GblReg,  322, "bG",     ShRI_GblReg,  1, None)
+ACSVM_CodeListACS0(ShRI_ModArr,  323, "bo",     ShRI_ModArr,  2, None)
+ACSVM_CodeListACS0(ShRI_HubArr,  324, "bu",     ShRI_HubArr,  2, None)
+ACSVM_CodeListACS0(ShRI_GblArr,  325, "bg",     ShRI_GblArr,  2, None)
+
+ACSVM_CodeListACS0(InvU,         330, "",       InvU,         1, None)
+
+ACSVM_CodeListACS0(PrintIntB,    349, "",       CallFunc,     1, PrintIntB)
+ACSVM_CodeListACS0(PrintIntX,    350, "",       CallFunc,     1, PrintIntX)
+ACSVM_CodeListACS0(CallFunc,     351, "bh",     CallFunc,     0, None)
+ACSVM_CodeListACS0(PrintEndStr,  352, "",       CallFunc,     0, PrintEndStr)
+ACSVM_CodeListACS0(PrintModArrR, 353, "",       CallFunc,     4, PrintModArr)
+ACSVM_CodeListACS0(PrintHubArrR, 354, "",       CallFunc,     4, PrintHubArr)
+ACSVM_CodeListACS0(PrintGblArrR, 355, "",       CallFunc,     4, PrintGblArr)
+ACSVM_CodeListACS0(StrCpyModArr, 356, "",       CallFunc,     6, StrCpyModArr)
+ACSVM_CodeListACS0(StrCpyHubArr, 357, "",       CallFunc,     6, StrCpyHubArr)
+ACSVM_CodeListACS0(StrCpyGblArr, 358, "",       CallFunc,     6, StrCpyGblArr)
+ACSVM_CodeListACS0(Pfun_Lit,     359, "b",      Pfun_Lit,     0, None)
+ACSVM_CodeListACS0(Call_Stk,     360, "",       Call_Stk,     1, None)
+ACSVM_CodeListACS0(ScrWaitS,     361, "",       ScrWaitS,     1, None)
+
+ACSVM_CodeListACS0(Jump_Stk,     363, "",       Jump_Stk,     1, None)
+ACSVM_CodeListACS0(Drop_LocArr,  364, "bl",     Drop_LocArr,  2, None)
+ACSVM_CodeListACS0(Push_LocArr,  365, "bl",     Push_LocArr,  1, None)
+ACSVM_CodeListACS0(AddU_LocArr,  366, "bl",     AddU_LocArr,  2, None)
+ACSVM_CodeListACS0(SubU_LocArr,  367, "bl",     SubU_LocArr,  2, None)
+ACSVM_CodeListACS0(MulU_LocArr,  368, "bl",     MulU_LocArr,  2, None)
+ACSVM_CodeListACS0(DivI_LocArr,  369, "bl",     DivI_LocArr,  2, None)
+ACSVM_CodeListACS0(ModI_LocArr,  370, "bl",     ModI_LocArr,  2, None)
+ACSVM_CodeListACS0(IncU_LocArr,  371, "bl",     IncU_LocArr,  2, None)
+ACSVM_CodeListACS0(DecU_LocArr,  372, "bl",     DecU_LocArr,  2, None)
+ACSVM_CodeListACS0(AndU_LocArr,  373, "bl",     AndU_LocArr,  2, None)
+ACSVM_CodeListACS0(OrXU_LocArr,  374, "bl",     OrXU_LocArr,  2, None)
+ACSVM_CodeListACS0(OrIU_LocArr,  375, "bl",     OrIU_LocArr,  2, None)
+ACSVM_CodeListACS0(ShLU_LocArr,  376, "bl",     ShLU_LocArr,  2, None)
+ACSVM_CodeListACS0(ShRI_LocArr,  377, "bl",     ShRI_LocArr,  2, None)
+ACSVM_CodeListACS0(PrintLocArr,  378, "",       CallFunc,     2, PrintLocArr)
+ACSVM_CodeListACS0(PrintLocArrR, 379, "",       CallFunc,     4, PrintLocArr)
+ACSVM_CodeListACS0(StrCpyLocArr, 380, "",       CallFunc,     6, StrCpyLocArr)
+
+ACSVM_CodeListACS0(CallSpec_5Ex, 381, "W",      CallSpec,     5, None)
+ACSVM_CodeListACS0(CallSpec_6,   500, "b",      CallSpec,     6, None)
+ACSVM_CodeListACS0(CallSpec_7,   501, "b",      CallSpec,     7, None)
+ACSVM_CodeListACS0(CallSpec_8,   502, "b",      CallSpec,     8, None)
+ACSVM_CodeListACS0(CallSpec_9,   503, "b",      CallSpec,     9, None)
+ACSVM_CodeListACS0(CallSpec_10,  504, "b",      CallSpec,    10, None)
+ACSVM_CodeListACS0(CallSpec_6L,  505, "bWWWWWW",      CallSpec_Lit, 0, None)
+ACSVM_CodeListACS0(CallSpec_7L,  506, "bWWWWWWWW",    CallSpec_Lit, 0, None)
+ACSVM_CodeListACS0(CallSpec_8L,  507, "bWWWWWWWWW",   CallSpec_Lit, 0, None)
+ACSVM_CodeListACS0(CallSpec_9L,  508, "bWWWWWWWWWW",  CallSpec_Lit, 0, None)
+ACSVM_CodeListACS0(CallSpec_10L, 509, "bWWWWWWWWWWW", CallSpec_Lit, 0, None)
+ACSVM_CodeListACS0(CallSpec_6LB, 510, "BBBBBBB",     CallSpec_Lit, 0, None)
+ACSVM_CodeListACS0(CallSpec_7LB, 511, "BBBBBBBB",    CallSpec_Lit, 0, None)
+ACSVM_CodeListACS0(CallSpec_8LB, 512, "BBBBBBBBB",   CallSpec_Lit, 0, None)
+ACSVM_CodeListACS0(CallSpec_9LB, 513, "BBBBBBBBBB",  CallSpec_Lit, 0, None)
+ACSVM_CodeListACS0(CallSpec_10LB,514, "BBBBBBBBBBB", CallSpec_Lit, 0, None)
+ACSVM_CodeListACS0(Push_Lit6B,   515, "BBBBBB",     Push_LitArr, 0, None)
+ACSVM_CodeListACS0(Push_Lit7B,   516, "BBBBBBB",    Push_LitArr, 0, None)
+ACSVM_CodeListACS0(Push_Lit8B,   517, "BBBBBBBB",   Push_LitArr, 0, None)
+ACSVM_CodeListACS0(Push_Lit9B,   518, "BBBBBBBBB",  Push_LitArr, 0, None)
+ACSVM_CodeListACS0(Push_Lit10B,  519, "BBBBBBBBBB", Push_LitArr, 0, None)
+ACSVM_CodeListACS0(CallSpec_10R1,520, "b",      CallSpec_R1, 10, None)
+
+#undef ACSVM_CodeListACS0
+#endif
+
+
+#ifdef ACSVM_FuncList
+
+ACSVM_FuncList(Nop)
+ACSVM_FuncList(Kill)
+
+// Printing functions.
+ACSVM_FuncList(PrintChar)
+ACSVM_FuncList(PrintEndStr)
+ACSVM_FuncList(PrintFixD)
+ACSVM_FuncList(PrintGblArr)
+ACSVM_FuncList(PrintHubArr)
+ACSVM_FuncList(PrintIntB)
+ACSVM_FuncList(PrintIntD)
+ACSVM_FuncList(PrintIntX)
+ACSVM_FuncList(PrintLocArr)
+ACSVM_FuncList(PrintModArr)
+ACSVM_FuncList(PrintPush)
+ACSVM_FuncList(PrintString)
+
+// Script functions.
+ACSVM_FuncList(ScrPauseS)
+ACSVM_FuncList(ScrStartS)
+ACSVM_FuncList(ScrStartSD) // Locked Door
+ACSVM_FuncList(ScrStartSF) // Forced
+ACSVM_FuncList(ScrStartSL) // Locked
+ACSVM_FuncList(ScrStartSR) // Result
+ACSVM_FuncList(ScrStopS)
+
+// String functions.
+ACSVM_FuncList(GetChar)
+ACSVM_FuncList(StrCaseCmp)
+ACSVM_FuncList(StrCmp)
+ACSVM_FuncList(StrCpyGblArr)
+ACSVM_FuncList(StrCpyHubArr)
+ACSVM_FuncList(StrCpyLocArr)
+ACSVM_FuncList(StrCpyModArr)
+ACSVM_FuncList(StrLeft)
+ACSVM_FuncList(StrLen)
+ACSVM_FuncList(StrMid)
+ACSVM_FuncList(StrRight)
+
+#undef ACSVM_FuncList
+#endif
+
+
+#ifdef ACSVM_FuncListACS0
+
+ACSVM_FuncListACS0(GetChar, 15, GetChar, {{2, Code::Push_StrArs}})
+
+ACSVM_FuncListACS0(ScrStartS,  39, ScrStartS,  {})
+ACSVM_FuncListACS0(ScrPauseS,  40, ScrPauseS,  {})
+ACSVM_FuncListACS0(ScrStopS,   41, ScrStopS,   {})
+ACSVM_FuncListACS0(ScrStartSL, 42, ScrStartSL, {})
+ACSVM_FuncListACS0(ScrStartSD, 43, ScrStartSD, {})
+ACSVM_FuncListACS0(ScrStartSR, 44, ScrStartSR, {})
+ACSVM_FuncListACS0(ScrStartSF, 45, ScrStartSF, {})
+
+ACSVM_FuncListACS0(StrCmp,     63, StrCmp,     {})
+ACSVM_FuncListACS0(StrCaseCmp, 64, StrCaseCmp, {})
+ACSVM_FuncListACS0(StrLeft,    65, StrLeft,    {})
+ACSVM_FuncListACS0(StrRight,   66, StrRight,   {})
+ACSVM_FuncListACS0(StrMid,     67, StrMid,     {})
+
+#undef ACSVM_FuncListACS0
+#endif
+
+// EOF
+
diff --git a/src/acs/vm/ACSVM/Environment.cpp b/src/acs/vm/ACSVM/Environment.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..6461ec5140059e3dab85f2f75af673e89634a2a2
--- /dev/null
+++ b/src/acs/vm/ACSVM/Environment.cpp
@@ -0,0 +1,908 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2015-2017 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// Environment class.
+//
+//-----------------------------------------------------------------------------
+
+#include "Environment.hpp"
+
+#include "Action.hpp"
+#include "BinaryIO.hpp"
+#include "CallFunc.hpp"
+#include "Code.hpp"
+#include "CodeData.hpp"
+#include "Function.hpp"
+#include "HashMap.hpp"
+#include "Module.hpp"
+#include "PrintBuf.hpp"
+#include "Scope.hpp"
+#include "Script.hpp"
+#include "Serial.hpp"
+#include "Thread.hpp"
+
+#include <iostream>
+#include <list>
+#include <unordered_map>
+#include <vector>
+
+
+//----------------------------------------------------------------------------|
+// Types                                                                      |
+//
+
+namespace ACSVM
+{
+   //
+   // Environment::PrivData
+   //
+   struct Environment::PrivData
+   {
+      using FuncName = std::pair<ModuleName, String *>;
+      using FuncElem = HashMapElem<FuncName, Word>;
+
+      struct NameEqual
+      {
+         bool operator () (ModuleName const *l, ModuleName const *r) const
+            {return *l == *r;}
+      };
+
+      struct NameHash
+      {
+         std::size_t operator () (ModuleName const *name) const
+            {return name->hash();}
+      };
+
+      struct FuncNameHash
+      {
+         std::size_t operator () (FuncName const &name) const
+            {return name.first.hash() + name.second->hash;}
+      };
+
+
+      // Reserve index 0 as no function.
+      std::vector<Function *> functionByIdx{nullptr};
+
+      HashMapKeyExt<FuncName, Word, FuncNameHash> functionByName{16, 16};
+
+      HashMapKeyMem<ModuleName, Module, &Module::name, &Module::hashLink> modules;
+
+      HashMapKeyMem<Word, GlobalScope, &GlobalScope::id, &GlobalScope::hashLink> scopes;
+
+      std::vector<CallFunc> tableCallFunc
+      {
+         #define ACSVM_FuncList(name) \
+            CallFunc_Func_##name,
+         #include "CodeList.hpp"
+
+         CallFunc_Func_Nop
+      };
+
+      std::unordered_map<Word, CodeDataACS0> tableCodeDataACS0
+      {
+         #define ACSVM_CodeListACS0(name, code, args, transCode, stackArgC, transFunc) \
+            {code, {CodeACS0::name, args, Code::transCode, stackArgC, Func::transFunc}},
+         #include "CodeList.hpp"
+      };
+
+      std::unordered_map<Word, FuncDataACS0> tableFuncDataACS0
+      {
+         #define ACSVM_FuncListACS0(name, func, transFunc, ...) \
+            {func, {FuncACS0::name, Func::transFunc, __VA_ARGS__}},
+         #include "CodeList.hpp"
+      };
+   };
+}
+
+
+//----------------------------------------------------------------------------|
+// Extern Functions                                                           |
+//
+
+namespace ACSVM
+{
+   //
+   // Environment constructor
+   //
+   Environment::Environment() :
+      branchLimit  {0},
+      scriptLocRegC{ScriptLocRegCDefault},
+
+      funcV{nullptr},
+      funcC{0},
+
+      pd{new PrivData}
+   {
+      funcV = pd->functionByIdx.data();
+      funcC = pd->functionByIdx.size();
+   }
+
+   //
+   // Environment destructor
+   //
+   Environment::~Environment()
+   {
+      pd->functionByName.free();
+      pd->modules.free();
+      pd->scopes.free();
+
+      while(scriptAction.next->obj)
+         delete scriptAction.next->obj;
+
+      delete pd;
+
+      // Deallocate threads. Do this after scopes have been destructed.
+      while(threadFree.next->obj)
+         delete threadFree.next->obj;
+   }
+
+   //
+   // Environment::addCallFunc
+   //
+   Word Environment::addCallFunc(CallFunc func)
+   {
+      pd->tableCallFunc.push_back(func);
+      return pd->tableCallFunc.size() - 1;
+   }
+
+   //
+   // Environment::addCodeDataACS0
+   //
+   void Environment::addCodeDataACS0(Word code, CodeDataACS0 &&data)
+   {
+      auto itr = pd->tableCodeDataACS0.find(code);
+      if(itr == pd->tableCodeDataACS0.end())
+         pd->tableCodeDataACS0.emplace(code, std::move(data));
+      else
+         itr->second = std::move(data);
+   }
+
+   //
+   // Environment::addFuncDataACS0
+   //
+   void Environment::addFuncDataACS0(Word func, FuncDataACS0 &&data)
+   {
+      auto itr = pd->tableFuncDataACS0.find(func);
+      if(itr == pd->tableFuncDataACS0.end())
+         pd->tableFuncDataACS0.emplace(func, std::move(data));
+      else
+         itr->second = std::move(data);
+   }
+
+   //
+   // Environment::allocThread
+   //
+   Thread *Environment::allocThread()
+   {
+      return new Thread(this);
+   }
+
+   //
+   // Environment::callFunc
+   //
+   bool Environment::callFunc(Thread *thread, Word func, Word const *argV, Word argC)
+   {
+      return pd->tableCallFunc[func](thread, argV, argC);
+   }
+
+   //
+   // Environment::callSpec
+   //
+   Word Environment::callSpec(Thread *thread, Word spec, Word const *argV, Word argC)
+   {
+      if(thread->scopeMap->clampCallSpec && thread->module->isACS0)
+      {
+         Vector<Word> argTmp{argV, argC};
+         for(auto &arg : argTmp) arg &= 0xFF;
+         return callSpecImpl(thread, spec, argTmp.data(), argTmp.size());
+      }
+      else
+         return callSpecImpl(thread, spec, argV, argC);
+   }
+
+   //
+   // Environment::callSpecImpl
+   //
+   Word Environment::callSpecImpl(Thread *, Word, Word const *, Word)
+   {
+      return 0;
+   }
+
+   //
+   // Environment::checkLock
+   //
+   bool Environment::checkLock(Thread *, Word, bool)
+   {
+      return false;
+   }
+
+   //
+   // Environment::checkTag
+   //
+   bool Environment::checkTag(Word, Word)
+   {
+      return false;
+   }
+
+   //
+   // Environment::collectStrings
+   //
+   void Environment::collectStrings()
+   {
+      stringTable.collectBegin();
+      refStrings();
+      stringTable.collectEnd();
+   }
+
+   //
+   // Environment::countActiveThread
+   //
+   std::size_t Environment::countActiveThread() const
+   {
+      std::size_t n = 0;
+
+      for(auto &scope : pd->scopes)
+      {
+         if(scope.active)
+            n += scope.countActiveThread();
+      }
+
+      return n;
+   }
+
+   //
+   // Environment::deferAction
+   //
+   void Environment::deferAction(ScriptAction &&action)
+   {
+      (new ScriptAction(std::move(action)))->link.insert(&scriptAction);
+   }
+
+   //
+   // Environment::exec
+   //
+   void Environment::exec()
+   {
+      // Delegate deferred script actions.
+      for(auto itr = scriptAction.begin(), end = scriptAction.end(); itr != end;)
+      {
+         auto scope = pd->scopes.find(itr->id.global);
+         if(scope && scope->active)
+            itr++->link.relink(&scope->scriptAction);
+         else
+            ++itr;
+      }
+
+      for(auto &scope : pd->scopes)
+      {
+         if(scope.active)
+            scope.exec();
+      }
+   }
+
+   //
+   // Environment::findCodeDataACS0
+   //
+   CodeDataACS0 const *Environment::findCodeDataACS0(Word code)
+   {
+      auto itr = pd->tableCodeDataACS0.find(code);
+      return itr == pd->tableCodeDataACS0.end() ? nullptr : &itr->second;
+   }
+
+   //
+   // Environment::findFuncDataACS0
+   //
+   FuncDataACS0 const *Environment::findFuncDataACS0(Word func)
+   {
+      auto itr = pd->tableFuncDataACS0.find(func);
+      return itr == pd->tableFuncDataACS0.end() ? nullptr : &itr->second;
+   }
+
+   //
+   // Environment::findModule
+   //
+   Module *Environment::findModule(ModuleName const &name) const
+   {
+      return pd->modules.find(name);
+   }
+
+   //
+   // Environment::freeFunction
+   //
+   void Environment::freeFunction(Function *func)
+   {
+      // Null every reference to this function in every Module.
+      // O(N*M) is not very nice, but that can be fixed if/when it comes up.
+      for(auto &module : pd->modules)
+      {
+         for(Function *&funcItr : module.functionV)
+         {
+            if(funcItr == func)
+               funcItr = nullptr;
+         }
+      }
+
+      pd->functionByIdx[func->idx] = nullptr;
+      delete func;
+   }
+
+   //
+   // Environment::freeGlobalScope
+   //
+   void Environment::freeGlobalScope(GlobalScope *scope)
+   {
+      pd->scopes.unlink(scope);
+      delete scope;
+   }
+
+   //
+   // Environment::freeModule
+   //
+   void Environment::freeModule(Module *module)
+   {
+      pd->modules.unlink(module);
+      delete module;
+   }
+
+   //
+   // Environment::freeThread
+   //
+   void Environment::freeThread(Thread *thread)
+   {
+      thread->link.relink(&threadFree);
+   }
+
+   //
+   // Environment::getCodeData
+   //
+   CodeData const *Environment::getCodeData(Code code)
+   {
+      switch(code)
+      {
+         #define ACSVM_CodeList(name, argc) case Code::name: \
+            {static CodeData const data{Code::name, argc}; return &data;}
+         #include "CodeList.hpp"
+
+      default:
+      case Code::None:
+         static CodeData const dataNone{Code::None, 0};
+         return &dataNone;
+      }
+   }
+
+   //
+   // Environment::getFreeThread
+   //
+   Thread *Environment::getFreeThread()
+   {
+      if(threadFree.next->obj)
+      {
+         Thread *thread = threadFree.next->obj;
+         thread->link.unlink();
+         return thread;
+      }
+      else
+         return allocThread();
+   }
+
+   //
+   // Environment::getFunction
+   //
+   Function *Environment::getFunction(Module *module, String *funcName)
+   {
+      if(funcName)
+      {
+         PrivData::FuncName namePair{module->name, funcName};
+         auto idx = pd->functionByName.find(namePair);
+
+         if(!idx)
+         {
+            #if SIZE_MAX > UINT32_MAX
+            if(pd->functionByIdx.size() > UINT32_MAX)
+               throw std::bad_alloc();
+            #endif
+
+            idx = new PrivData::FuncElem{std::move(namePair),
+               static_cast<Word>(pd->functionByIdx.size())};
+            pd->functionByName.insert(idx);
+
+            pd->functionByIdx.emplace_back();
+            funcV = pd->functionByIdx.data();
+            funcC = pd->functionByIdx.size();
+         }
+
+         auto &ptr = pd->functionByIdx[idx->val];
+
+         if(!ptr)
+            ptr = new Function{module, funcName, idx->val};
+
+         return ptr;
+      }
+      else
+         return new Function{module, nullptr, 0};
+   }
+
+   //
+   // Environment::getGlobalScope
+   //
+   GlobalScope *Environment::getGlobalScope(Word id)
+   {
+      if(auto *scope = pd->scopes.find(id))
+         return scope;
+
+      auto scope = new GlobalScope(this, id);
+      pd->scopes.insert(scope);
+      return scope;
+   }
+
+   //
+   // Environment::getModule
+   //
+   Module *Environment::getModule(ModuleName const &name)
+   {
+      auto module = pd->modules.find(name);
+
+      if(!module)
+      {
+         module = new Module{this, name};
+         pd->modules.insert(module);
+         loadModule(module);
+      }
+      else
+      {
+         if(!module->loaded)
+            loadModule(module);
+      }
+
+      return module;
+   }
+
+   //
+   // Environment::getModuleName
+   //
+   ModuleName Environment::getModuleName(char const *str)
+   {
+      return getModuleName(str, std::strlen(str));
+   }
+
+   //
+   // Environment::getModuleName
+   //
+   ModuleName Environment::getModuleName(char const *str, std::size_t len)
+   {
+      return {getString(str, len), nullptr, 0};
+   }
+
+   //
+   // Environment::hasActiveThread
+   //
+   bool Environment::hasActiveThread() const
+   {
+      for(auto &scope : pd->scopes)
+      {
+         if(scope.active && scope.hasActiveThread())
+            return true;
+      }
+
+      return false;
+   }
+
+   //
+   // Environment::loadFunctions
+   //
+   void Environment::loadFunctions(Serial &in)
+   {
+      // Function index map.
+      pd->functionByName.free();
+      for(std::size_t n = ReadVLN<std::size_t>(in); n--;)
+      {
+         ModuleName name = readModuleName(in);
+         String    *str  = &stringTable[ReadVLN<Word>(in)];
+         Word       idx  = ReadVLN<Word>(in);
+
+         pd->functionByName.insert(new PrivData::FuncElem{{name, str}, idx});
+      }
+
+      // Function vector.
+      auto oldTable = pd->functionByIdx;
+
+      pd->functionByIdx.clear();
+      pd->functionByIdx.resize(ReadVLN<std::size_t>(in), nullptr);
+      funcV = pd->functionByIdx.data();
+      funcC = pd->functionByIdx.size();
+
+      // Reset function indexes.
+      for(Function *&func : oldTable)
+      {
+         if(func)
+         {
+            auto idx = pd->functionByName.find({func->module->name, func->name});
+            func->idx = idx ? idx->val : 0;
+            pd->functionByIdx[func->idx] = func;
+         }
+      }
+   }
+
+   //
+   // Environment::loadGlobalScopes
+   //
+   void Environment::loadGlobalScopes(Serial &in)
+   {
+      // Clear existing scopes.
+      pd->scopes.free();
+
+      for(auto n = ReadVLN<std::size_t>(in); n--;)
+         getGlobalScope(ReadVLN<Word>(in))->loadState(in);
+   }
+
+   //
+   // Environment::loadScriptActions
+   //
+   void Environment::loadScriptActions(Serial &in)
+   {
+      readScriptActions(in, scriptAction);
+   }
+
+   //
+   // Environment::loadState
+   //
+   void Environment::loadState(Serial &in)
+   {
+      in.readSign(Signature::Environment);
+
+      loadStringTable(in);
+      loadFunctions(in);
+      loadGlobalScopes(in);
+      loadScriptActions(in);
+
+      in.readSign(~Signature::Environment);
+   }
+
+   //
+   // Environment::loadStringTable
+   //
+   void Environment::loadStringTable(Serial &in)
+   {
+      StringTable oldTable{std::move(stringTable)};
+      stringTable.loadState(in);
+      resetStrings();
+   }
+
+   //
+   // Environment::printArray
+   //
+   void Environment::printArray(PrintBuf &buf, Array const &array, Word index, Word limit)
+   {
+      PrintArrayChar(buf, array, index, limit);
+   }
+
+   //
+   // Environment::printKill
+   //
+   void Environment::printKill(Thread *thread, Word type, Word data)
+   {
+      std::cerr << "ACSVM ERROR: Kill " << type << ':' << data
+         << " at " << (thread->codePtr - thread->module->codeV.data() - 1) << '\n';
+   }
+
+   //
+   // Environment::readModuleName
+   //
+   ModuleName Environment::readModuleName(Serial &in) const
+   {
+      auto s = readString(in);
+      auto i = ReadVLN<std::size_t>(in);
+
+      return {s, nullptr, i};
+   }
+
+   //
+   // Environment::readScript
+   //
+   Script *Environment::readScript(Serial &in) const
+   {
+      auto idx = ReadVLN<std::size_t>(in);
+      return &findModule(readModuleName(in))->scriptV[idx];
+   }
+
+   //
+   // Environment::readScriptAction
+   //
+   ScriptAction *Environment::readScriptAction(Serial &in) const
+   {
+      auto action = static_cast<ScriptAction::Action>(ReadVLN<int>(in));
+
+      Vector<Word> argV;
+      argV.alloc(ReadVLN<std::size_t>(in));
+      for(auto &arg : argV)
+         arg = ReadVLN<Word>(in);
+
+      ScopeID id;
+      id.global = ReadVLN<Word>(in);
+      id.hub    = ReadVLN<Word>(in);
+      id.map    = ReadVLN<Word>(in);
+
+      ScriptName name = readScriptName(in);
+
+      return new ScriptAction{id, name, action, std::move(argV)};
+   }
+
+   //
+   // Environment::readScriptActions
+   //
+   void Environment::readScriptActions(Serial &in, ListLink<ScriptAction> &out) const
+   {
+      // Clear existing actions.
+      while(out.next->obj)
+         delete out.next->obj;
+
+      for(auto n = ReadVLN<std::size_t>(in); n--;)
+         readScriptAction(in)->link.insert(&out);
+   }
+
+   //
+   // Environment::readScriptName
+   //
+   ScriptName Environment::readScriptName(Serial &in) const
+   {
+      String *s = in.in->get() ? &stringTable[ReadVLN<Word>(in)] : nullptr;
+      Word    i = ReadVLN<Word>(in);
+      return {s, i};
+   }
+
+   //
+   // Environment::readString
+   //
+   String *Environment::readString(Serial &in) const
+   {
+      if(auto idx = ReadVLN<std::size_t>(in))
+         return &stringTable[idx - 1];
+      else
+         return nullptr;
+   }
+
+   //
+   // Environment::refStrings
+   //
+   void Environment::refStrings()
+   {
+      for(auto &action : scriptAction)
+         action.refStrings(this);
+
+      for(auto &funcIdx : pd->functionByName)
+      {
+         funcIdx.key.first.s->ref = true;
+         funcIdx.key.second->ref  = true;
+      }
+
+      for(auto &module : pd->modules)
+         module.refStrings();
+
+      for(auto &scope : pd->scopes)
+         scope.refStrings();
+   }
+
+   //
+   // Environment::resetStrings
+   //
+   void Environment::resetStrings()
+   {
+      for(auto &funcIdx : pd->functionByName)
+      {
+         funcIdx.key.first.s = getString(funcIdx.key.first.s);
+         funcIdx.key.second  = getString(funcIdx.key.second);
+      }
+
+      for(auto &module : pd->modules)
+         module.resetStrings();
+   }
+
+   //
+   // Environment::saveFunctions
+   //
+   void Environment::saveFunctions(Serial &out) const
+   {
+      WriteVLN(out, pd->functionByName.size());
+      for(auto &funcIdx : pd->functionByName)
+      {
+         writeModuleName(out, funcIdx.key.first);
+         WriteVLN(out, funcIdx.key.second->idx);
+         WriteVLN(out, funcIdx.val);
+      }
+
+      WriteVLN(out, pd->functionByIdx.size());
+   }
+
+   //
+   // Environment::saveGlobalScopes
+   //
+   void Environment::saveGlobalScopes(Serial &out) const
+   {
+      WriteVLN(out, pd->scopes.size());
+      for(auto &scope : pd->scopes)
+      {
+         WriteVLN(out, scope.id);
+         scope.saveState(out);
+      }
+   }
+
+   //
+   // Environment::saveScriptActions
+   //
+   void Environment::saveScriptActions(Serial &out) const
+   {
+      writeScriptActions(out, scriptAction);
+   }
+
+   //
+   // Environment::saveState
+   //
+   void Environment::saveState(Serial &out) const
+   {
+      out.writeSign(Signature::Environment);
+
+      saveStringTable(out);
+      saveFunctions(out);
+      saveGlobalScopes(out);
+      saveScriptActions(out);
+
+      out.writeSign(~Signature::Environment);
+   }
+
+   //
+   // Environment::saveStringTable
+   //
+   void Environment::saveStringTable(Serial &out) const
+   {
+      stringTable.saveState(out);
+   }
+
+   //
+   // Environment::writeModuleName
+   //
+   void Environment::writeModuleName(Serial &out, ModuleName const &in) const
+   {
+      writeString(out, in.s);
+      WriteVLN(out, in.i);
+   }
+
+   //
+   // Environment::writeScript
+   //
+   void Environment::writeScript(Serial &out, Script *in) const
+   {
+      WriteVLN(out, in - in->module->scriptV.data());
+      writeModuleName(out, in->module->name);
+   }
+
+   //
+   // Environment::writeScriptAction
+   //
+   void Environment::writeScriptAction(Serial &out, ScriptAction const *in) const
+   {
+      WriteVLN<int>(out, in->action);
+
+      WriteVLN(out, in->argV.size());
+      for(auto &arg : in->argV)
+         WriteVLN(out, arg);
+
+      WriteVLN(out, in->id.global);
+      WriteVLN(out, in->id.hub);
+      WriteVLN(out, in->id.map);
+
+      writeScriptName(out, in->name);
+   }
+
+   //
+   // Environment::writeScriptActions
+   //
+   void Environment::writeScriptActions(Serial &out,
+      ListLink<ScriptAction> const &in) const
+   {
+      WriteVLN(out, in.size());
+
+      for(auto &action : in)
+         writeScriptAction(out, &action);
+   }
+
+   //
+   // Environment::writeScriptName
+   //
+   void Environment::writeScriptName(Serial &out, ScriptName const &in) const
+   {
+      if(in.s)
+      {
+         out.out->put('\1');
+         WriteVLN(out, in.s->idx);
+      }
+      else
+         out.out->put('\0');
+
+      WriteVLN(out, in.i);
+   }
+
+   //
+   // Environment::writeString
+   //
+   void Environment::writeString(Serial &out, String const *in) const
+   {
+      if(in)
+         WriteVLN<std::size_t>(out, in->idx + 1);
+      else
+         WriteVLN<std::size_t>(out, 0);
+   }
+
+   //
+   // Environment::PrintArrayChar
+   //
+   void Environment::PrintArrayChar(PrintBuf &buf, Array const &array, Word index, Word limit)
+   {
+      // Calculate output length and end index.
+      std::size_t len = 0;
+      Word        end;
+      for(Word &itr = end = index; itr - index != limit; ++itr)
+      {
+         Word c = array.find(itr);
+         if(!c) break;
+         ++len;
+      }
+
+      // Acquire output buffer.
+      buf.reserve(len);
+      char *s = buf.getBuf(len);
+
+      // Truncate elements to char.
+      for(Word itr = index; itr != end; ++itr)
+         *s++ = array.find(itr);
+   }
+
+   //
+   // Environment::PrintArrayUTF8
+   //
+   void Environment::PrintArrayUTF8(PrintBuf &buf, Array const &array, Word index, Word limit)
+   {
+      // Calculate output length and end index.
+      std::size_t len = 0;
+      Word        end;
+      for(Word &itr = end = index; itr - index != limit; ++itr)
+      {
+         Word c = array.find(itr);
+         if(!c) break;
+         if(c > 0x10FFFF) c = 0xFFFD;
+
+              if(c <= 0x007F) len += 1;
+         else if(c <= 0x07FF) len += 2;
+         else if(c <= 0xFFFF) len += 3;
+         else                 len += 4;
+      }
+
+      // Acquire output buffer.
+      buf.reserve(len);
+      char *s = buf.getBuf(len);
+
+      // Convert UTF-32 sequence to UTF-8.
+      for(Word itr = index; itr != end; ++itr)
+      {
+         Word c = array.find(itr);
+         if(c > 0x10FFFF) c = 0xFFFD;
+
+         if(c <= 0x7F)   {*s++ = 0x00 | (c >>  0); goto put0;}
+         if(c <= 0x7FF)  {*s++ = 0xC0 | (c >>  6); goto put1;}
+         if(c <= 0xFFFF) {*s++ = 0xE0 | (c >> 12); goto put2;}
+                         {*s++ = 0xF0 | (c >> 18); goto put3;}
+
+         put3: *s++ = 0x80 | ((c >> 12) & 0x3F);
+         put2: *s++ = 0x80 | ((c >>  6) & 0x3F);
+         put1: *s++ = 0x80 | ((c >>  0) & 0x3F);
+         put0:;
+      }
+   }
+}
+
+// EOF
+
diff --git a/src/acs/vm/ACSVM/Environment.hpp b/src/acs/vm/ACSVM/Environment.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..975a5af88375b9880e29ac9b1a6b1fb5f6c7a296
--- /dev/null
+++ b/src/acs/vm/ACSVM/Environment.hpp
@@ -0,0 +1,203 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2015-2017 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// Environment class.
+//
+//-----------------------------------------------------------------------------
+
+#ifndef ACSVM__Environment_H__
+#define ACSVM__Environment_H__
+
+#include "List.hpp"
+#include "String.hpp"
+
+
+//----------------------------------------------------------------------------|
+// Types                                                                      |
+//
+
+namespace ACSVM
+{
+   //
+   // Environment
+   //
+   // Represents an entire ACS environment.
+   //
+   class Environment
+   {
+   public:
+      Environment();
+      virtual ~Environment();
+
+      Word addCallFunc(CallFunc func);
+
+      void addCodeDataACS0(Word code, CodeDataACS0 &&data);
+      void addFuncDataACS0(Word func, FuncDataACS0 &&data);
+
+      virtual bool callFunc(Thread *thread, Word func, Word const *argV, Word argC);
+      Word callSpec(Thread *thread, Word spec, Word const *argV, Word argC);
+
+      // Function to check if a lock can be opened. Default behavior is to
+      // always return false.
+      virtual bool checkLock(Thread *thread, Word lock, bool door);
+
+      // Function to check tags. Must return true to indicate script should
+      // continue. Default behavior is to always return false.
+      virtual bool checkTag(Word type, Word tag);
+
+      void collectStrings();
+
+      std::size_t countActiveThread() const;
+
+      void deferAction(ScriptAction &&action);
+
+      virtual void exec();
+
+      CodeDataACS0 const *findCodeDataACS0(Word code);
+      FuncDataACS0 const *findFuncDataACS0(Word func);
+
+      Module *findModule(ModuleName const &name) const;
+
+      // Used by Module when unloading.
+      void freeFunction(Function *func);
+
+      void freeGlobalScope(GlobalScope *scope);
+
+      void freeModule(Module *module);
+
+      void freeThread(Thread *thread);
+
+      CodeData const *getCodeData(Code code);
+
+      Thread *getFreeThread();
+
+      Function *getFunction(Word idx) {return idx < funcC ? funcV[idx] : nullptr;}
+
+      Function *getFunction(Module *module, String *name);
+
+      GlobalScope *getGlobalScope(Word id);
+
+      // Gets the named module, loading it if needed.
+      Module *getModule(ModuleName const &name);
+
+      ModuleName getModuleName(char const *str);
+      virtual ModuleName getModuleName(char const *str, std::size_t len);
+
+      // Called to translate script type from ACS0 script number.
+      // Default behavior is to modulus 1000 the name.
+      virtual std::pair<Word /*type*/, Word /*name*/> getScriptTypeACS0(Word name)
+         {return {name / 1000, name % 1000};}
+
+      // Called to translate script type from ACSE SPTR.
+      // Default behavior is to return the type as-is.
+      virtual Word getScriptTypeACSE(Word type) {return type;}
+
+      String *getString(Word idx) {return &stringTable[~idx];}
+
+      String *getString(char const *first, char const *last)
+         {return &stringTable[{first, last}];}
+
+      String *getString(char const *str)
+         {return getString(str, std::strlen(str));}
+
+      String *getString(char const *str, std::size_t len)
+         {return &stringTable[{str, len}];}
+
+      String *getString(StringData const *data)
+         {return data ? &stringTable[*data] : nullptr;}
+
+      // Returns true if any contained scope is active and has an active thread.
+      bool hasActiveThread() const;
+
+      virtual void loadState(Serial &in);
+
+      // Prints an array to a print buffer. Default behavior is PrintArrayChar.
+      virtual void printArray(PrintBuf &buf, Array const &array, Word index, Word limit);
+
+      // Function to print Kill instructions. Default behavior is to print
+      // message to stderr.
+      virtual void printKill(Thread *thread, Word type, Word data);
+
+      // Deserializes a ModuleName. Default behavior is to load s and i.
+      virtual ModuleName readModuleName(Serial &in) const;
+
+      Script *readScript(Serial &in) const;
+      ScriptAction *readScriptAction(Serial &in) const;
+      void readScriptActions(Serial &in, ListLink<ScriptAction> &out) const;
+      ScriptName readScriptName(Serial &in) const;
+      String *readString(Serial &in) const;
+
+      virtual void refStrings();
+
+      virtual void resetStrings();
+
+      virtual void saveState(Serial &out) const;
+
+      // Serializes a ModuleName. Default behavior is to save s and i.
+      virtual void writeModuleName(Serial &out, ModuleName const &name) const;
+
+      void writeScript(Serial &out, Script *in) const;
+      void writeScriptAction(Serial &out, ScriptAction const *in) const;
+      void writeScriptActions(Serial &out, ListLink<ScriptAction> const &in) const;
+      void writeScriptName(Serial &out, ScriptName const &in) const;
+      void writeString(Serial &out, String const *in) const;
+
+      StringTable stringTable;
+
+      // Number of branches allowed per call to Thread::exec. Default of 0
+      // means no limit.
+      Word branchLimit;
+
+      // Default number of script variables. Default is 20.
+      Word scriptLocRegC;
+
+
+      // Prints an array to a print buffer, truncating elements of the array to
+      // fit char.
+      static void PrintArrayChar(PrintBuf &buf, Array const &array, Word index, Word limit);
+
+      // Prints an array to a print buffer, converting the array as a UTF-32
+      // sequence into a UTF-8 sequence.
+      static void PrintArrayUTF8(PrintBuf &buf, Array const &array, Word index, Word limit);
+
+      static constexpr Word ScriptLocRegCDefault = 20;
+
+   protected:
+      virtual Thread *allocThread();
+
+      // Called by callSpec after processing arguments. Default behavior is to
+      // do nothing and return 0.
+      virtual Word callSpecImpl(Thread *thread, Word spec, Word const *argV, Word argC);
+
+      virtual void loadModule(Module *module) = 0;
+
+      ListLink<ScriptAction> scriptAction;
+      ListLink<Thread>       threadFree;
+
+      Function  **funcV;
+      std::size_t funcC;
+
+   private:
+      struct PrivData;
+
+      void loadFunctions(Serial &in);
+      void loadGlobalScopes(Serial &in);
+      void loadScriptActions(Serial &in);
+      void loadStringTable(Serial &in);
+
+      void saveFunctions(Serial &out) const;
+      void saveGlobalScopes(Serial &out) const;
+      void saveScriptActions(Serial &out) const;
+      void saveStringTable(Serial &out) const;
+
+      PrivData *pd;
+   };
+}
+
+#endif//ACSVM__Environment_H__
+
diff --git a/src/acs/vm/ACSVM/Error.cpp b/src/acs/vm/ACSVM/Error.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..85de5cbbd14421282882db4417605322f757591d
--- /dev/null
+++ b/src/acs/vm/ACSVM/Error.cpp
@@ -0,0 +1,56 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2017 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// Error classes.
+//
+//-----------------------------------------------------------------------------
+
+#include "Error.hpp"
+
+#include "BinaryIO.hpp"
+
+#include <cctype>
+#include <cstdio>
+
+
+//----------------------------------------------------------------------------|
+// Extern Functions                                                           |
+//
+
+namespace ACSVM
+{
+   //
+   // SerialSignError constructor
+   //
+   SerialSignError::SerialSignError(Signature sig_, Signature got_)
+   {
+      auto sig = static_cast<std::uint32_t>(sig_);
+      auto got = static_cast<std::uint32_t>(got_);
+
+      WriteLE4(reinterpret_cast<Byte *>(buf + SigS), sig);
+      WriteLE4(reinterpret_cast<Byte *>(buf + GotS), got);
+      for(auto i : {SigS+0, SigS+1, SigS+2, SigS+3, GotS+0, GotS+1, GotS+2, GotS+3})
+         if(!std::isprint(buf[i]) && !std::isprint(buf[i] = ~buf[i])) buf[i] = ' ';
+
+      for(int i = 8; i--;) buf[Sig + i] = "0123456789ABCDEF"[sig & 0xF], sig >>= 4;
+      for(int i = 8; i--;) buf[Got + i] = "0123456789ABCDEF"[got & 0xF], got >>= 4;
+
+      msg = buf;
+   }
+
+   //
+   // SerialSignError destructor
+   //
+   SerialSignError::~SerialSignError()
+   {
+      delete[] msg;
+   }
+}
+
+// EOF
+
diff --git a/src/acs/vm/ACSVM/Error.hpp b/src/acs/vm/ACSVM/Error.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..6d217a11158b531c005e5f0517486821e6ab642a
--- /dev/null
+++ b/src/acs/vm/ACSVM/Error.hpp
@@ -0,0 +1,81 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2015-2017 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// Error classes.
+//
+//-----------------------------------------------------------------------------
+
+#ifndef ACSVM__Error_H__
+#define ACSVM__Error_H__
+
+#include "Types.hpp"
+
+#include <stdexcept>
+
+
+//----------------------------------------------------------------------------|
+// Types                                                                      |
+//
+
+namespace ACSVM
+{
+   //
+   // ReadError
+   //
+   // Generic exception class for errors occurring during bytecode reading.
+   //
+   class ReadError : public std::exception
+   {
+   public:
+      ReadError(char const *msg_ = "ACSVM::ReadError") : msg{msg_} {}
+
+      virtual char const *what() const noexcept {return msg;}
+
+      char const *const msg;
+   };
+
+   //
+   // SerialError
+   //
+   // Generic exception for errors during serialization.
+   //
+   class SerialError : public std::exception
+   {
+   public:
+      SerialError(char const *msg_) : msg{const_cast<char *>(msg_)} {}
+
+      virtual char const *what() const noexcept {return msg;}
+
+   protected:
+      SerialError() : msg{nullptr} {}
+
+      char *msg;
+   };
+
+   //
+   // SerialSignError
+   //
+   // Thrown due to signature mismatch.
+   //
+   class SerialSignError : public SerialError
+   {
+   public:
+      SerialSignError(Signature sig, Signature got);
+      ~SerialSignError();
+
+   private:
+      static constexpr std::size_t Sig = 29, SigS = 39;
+      static constexpr std::size_t Got = 49, GotS = 59;
+
+      char buf[sizeof("signature mismatch: expected XXXXXXXX (XXXX) got XXXXXXXX (XXXX)")] =
+                      "signature mismatch: expected XXXXXXXX (XXXX) got XXXXXXXX (XXXX)";
+   };
+}
+
+#endif//ACSVM__Error_H__
+
diff --git a/src/acs/vm/ACSVM/Function.cpp b/src/acs/vm/ACSVM/Function.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..53e5e8f6c65d47475b706e5bb0c3ba0895213a35
--- /dev/null
+++ b/src/acs/vm/ACSVM/Function.cpp
@@ -0,0 +1,48 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2015 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// Function class.
+//
+//-----------------------------------------------------------------------------
+
+#include "Function.hpp"
+
+
+//----------------------------------------------------------------------------|
+// Extern Functions                                                           |
+//
+
+namespace ACSVM
+{
+   //
+   // Function constructor
+   //
+   Function::Function(Module *module_, String *name_, Word idx_) :
+      module{module_},
+      name  {name_},
+      idx   {idx_},
+
+      argC   {0},
+      codeIdx{0},
+      locArrC{0},
+      locRegC{0},
+
+      flagRet{false}
+   {
+   }
+
+   //
+   // Function destructor
+   //
+   Function::~Function()
+   {
+   }
+}
+
+// EOF
+
diff --git a/src/acs/vm/ACSVM/Function.hpp b/src/acs/vm/ACSVM/Function.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..2186d537ba7aa6f26484beda728f03adf417d88e
--- /dev/null
+++ b/src/acs/vm/ACSVM/Function.hpp
@@ -0,0 +1,48 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2015 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// Function class.
+//
+//-----------------------------------------------------------------------------
+
+#ifndef ACSVM__Function_H__
+#define ACSVM__Function_H__
+
+#include "Types.hpp"
+
+
+//----------------------------------------------------------------------------|
+// Types                                                                      |
+//
+
+namespace ACSVM
+{
+   //
+   // Function
+   //
+   class Function
+   {
+   public:
+      Function(Module *module, String *name, Word idx);
+      ~Function();
+
+      Module *module;
+      String *name;
+      Word    idx;
+
+      Word    argC;
+      Word    codeIdx;
+      Word    locArrC;
+      Word    locRegC;
+
+      bool flagRet : 1;
+   };
+}
+
+#endif//ACSVM__Function_H__
+
diff --git a/src/acs/vm/ACSVM/HashMap.hpp b/src/acs/vm/ACSVM/HashMap.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..b509e5119e10171b37c627be4442758aed461bf0
--- /dev/null
+++ b/src/acs/vm/ACSVM/HashMap.hpp
@@ -0,0 +1,278 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2015 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// HashMap class.
+//
+//-----------------------------------------------------------------------------
+
+#ifndef ACSVM__HashMap_H__
+#define ACSVM__HashMap_H__
+
+#include "List.hpp"
+#include "Types.hpp"
+#include "Vector.hpp"
+
+#include <functional>
+
+
+//----------------------------------------------------------------------------|
+// Types                                                                      |
+//
+
+namespace ACSVM
+{
+   //
+   // HashMapGetKeyMem
+   //
+   // Used for HashMaps for which the Key is a member of T.
+   //
+   template<typename Key, typename T, Key const T::*KeyMem>
+   struct HashMapGetKeyMem
+   {
+      static Key const &Get(T *obj) {return obj->*KeyMem;}
+   };
+
+   //
+   // HashMapGetKeyObj
+   //
+   // Used for HashMaps for which the Key is a base class or the same as T.
+   //
+   template<typename Key, typename T>
+   struct HashMapGetKeyObj
+   {
+      static Key const &Get(T *obj) {return *obj;}
+   };
+
+   //
+   // HashMap
+   //
+   // Stores objects of type T that can be found by keys of type Key.
+   //
+   // GetKeyMem must be HashMapGetKeyMem or HashMapGetKeyObj.
+   //
+   // This class does not manage the lifetimes of the contained objects, and
+   // objects must be unlinked by the unlink function before being destructed.
+   // Although an exception is made to the latter if the clear function is
+   // called before any other.
+   //
+   template<typename Key, typename T, typename GetKey, ListLink<T> T::*LinkMem,
+      typename Hash = std::hash<Key>, typename KeyEqual = std::equal_to<Key>>
+   class HashMap
+   {
+   private:
+      //
+      // Iterator
+      //
+      template<typename Obj>
+      class IteratorBase
+      {
+      public:
+         //
+         // operator ++
+         //
+         IteratorBase<Obj> &operator ++ ()
+         {
+            for(;;)
+            {
+               // Traverse to next link.
+               link = link->next;
+
+               // If it has an object, we are done.
+               if(link->obj) break;
+
+               // Otherwise, we are at the current chain's head. So increment
+               // to the next chain. If at the last chain, we are done.
+               if(++link == last) break;
+            }
+
+            return *this;
+         }
+
+         IteratorBase<Obj> operator ++ (int) {auto i = *this; ++*this; return i;}
+
+         Obj &operator * () const {return *link->obj;}
+         Obj *operator -> () const {return link->obj;}
+
+         bool operator == (IteratorBase<Obj> const &iter) const
+            {return iter.link == link;}
+         bool operator != (IteratorBase<Obj> const &iter) const
+            {return iter.link != link;}
+
+
+         friend class HashMap;
+
+      private:
+         IteratorBase(ListLink<T> *link_, ListLink<T> *last_) :
+            link{link_}, last{last_} {if(link != last) ++*this;}
+
+         ListLink<T> *link, *last;
+      };
+
+   public:
+      using const_iterator = IteratorBase<T const>;
+      using iterator       = IteratorBase<T>;
+      using size_type      = std::size_t;
+
+
+      HashMap() : chainV{16}, objC{0}, growC{16} {}
+      HashMap(size_type count, size_type growC_) :
+         chainV{count}, objC{0}, growC{growC_} {}
+      ~HashMap() {clear();}
+
+      // begin
+      iterator begin() {return {chainV.begin(), chainV.end()};}
+
+      //
+      // clear
+      //
+      void clear()
+      {
+         for(auto &chain : chainV)
+         {
+            while(auto obj = chain.next->obj)
+               (obj->*LinkMem).unlink();
+         }
+
+         objC = 0;
+      }
+
+      // end
+      iterator end() {return {chainV.end(), chainV.end()};}
+
+      //
+      // find
+      //
+      T *find(Key const &key)
+      {
+         for(auto itr = chainV[hasher(key) % chainV.size()].next; itr->obj; itr = itr->next)
+         {
+            if(equal(key, GetKey::Get(itr->obj)))
+               return itr->obj;
+         }
+
+         return nullptr;
+      }
+
+      //
+      // free
+      //
+      // Unlinks and deletes all contained objects.
+      //
+      void free()
+      {
+         for(auto &chain : chainV)
+         {
+            while(auto obj = chain.next->obj)
+               (obj->*LinkMem).unlink(), delete obj;
+         }
+
+         objC = 0;
+      }
+
+      //
+      // insert
+      //
+      void insert(T *obj)
+      {
+         if(objC >= chainV.size())
+            resize(chainV.size() + chainV.size() / 2 + growC);
+
+         ++objC;
+         (obj->*LinkMem).insert(&chainV[hasher(GetKey::Get(obj)) % chainV.size()]);
+      }
+
+      //
+      // resize
+      //
+      // Reallocates to count chains.
+      //
+      void resize(size_type count)
+      {
+         auto oldChainV = std::move(chainV);
+         chainV.alloc(count);
+
+         for(auto &chain : oldChainV)
+         {
+            while(auto obj = chain.next->obj)
+               (obj->*LinkMem).relink(&chainV[hasher(GetKey::Get(obj)) % chainV.size()]);
+         }
+      }
+
+      // size
+      size_type size() const {return objC;}
+
+      //
+      // unlink
+      //
+      void unlink(T *obj)
+      {
+         --objC;
+         (obj->*LinkMem).unlink();
+      }
+
+   private:
+      Vector<ListLink<T>> chainV;
+      Hash                hasher;
+      KeyEqual            equal;
+
+      size_type objC;
+      size_type growC;
+   };
+
+   //
+   // HashMapKeyMem
+   //
+   // Convenience typedef for HashMapGetKeyMem-based HashMaps.
+   //
+   template<typename Key, typename T, Key const T::*KeyMem,
+      ListLink<T> T::*LinkMem, typename Hash = std::hash<Key>,
+      typename KeyEqual = std::equal_to<Key>>
+   using HashMapKeyMem = HashMap<Key, T, HashMapGetKeyMem<Key, T, KeyMem>,
+      LinkMem, Hash, KeyEqual>;
+
+   //
+   // HashMapKeyObj
+   //
+   // Convenience typedef for HashMapGetKeyObj-based HashMaps.
+   //
+   template<typename Key, typename T, ListLink<T> T::*LinkMem,
+      typename Hash = std::hash<Key>, typename KeyEqual = std::equal_to<Key>>
+   using HashMapKeyObj = HashMap<Key, T, HashMapGetKeyObj<Key, T>, LinkMem,
+      Hash, KeyEqual>;
+
+   //
+   // HashMapElem
+   //
+   // Wraps a type with a key and link for use in HashMap.
+   //
+   template<typename Key, typename T>
+   class HashMapElem
+   {
+   public:
+      HashMapElem(Key const &key_, T const &val_) :
+         key{key_}, val{val_}, link{this} {}
+
+      Key key;
+      T   val;
+
+      ListLink<HashMapElem<Key, T>> link;
+   };
+
+   //
+   // HashMapKeyExt
+   //
+   // Convenience typedef for HashMapElem-based HashMaps.
+   //
+   template<typename Key, typename T, typename Hash = std::hash<Key>,
+      typename KeyEqual = std::equal_to<Key>>
+   using HashMapKeyExt = HashMapKeyMem<Key, HashMapElem<Key, T>,
+      &HashMapElem<Key, T>::key, &HashMapElem<Key, T>::link, Hash, KeyEqual>;
+}
+
+#endif//ACSVM__HashMap_H__
+
diff --git a/src/acs/vm/ACSVM/HashMapFixed.hpp b/src/acs/vm/ACSVM/HashMapFixed.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..0886d9b97dfa2cc477c1829c646fda12d0390df0
--- /dev/null
+++ b/src/acs/vm/ACSVM/HashMapFixed.hpp
@@ -0,0 +1,144 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2015 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// HashMapFixed class.
+//
+//-----------------------------------------------------------------------------
+
+#ifndef ACSVM__HashMapFixed_H__
+#define ACSVM__HashMapFixed_H__
+
+#include "Types.hpp"
+
+#include <functional>
+#include <new>
+
+
+//----------------------------------------------------------------------------|
+// Types                                                                      |
+//
+
+namespace ACSVM
+{
+   //
+   // HashMapFixed
+   //
+   // Non-resizable hash map.
+   //
+   template<typename Key, typename T, typename Hash = std::hash<Key>>
+   class HashMapFixed
+   {
+   public:
+      struct Elem
+      {
+         Key   key;
+         T     val;
+         Elem *next;
+      };
+
+      using iterator   = Elem *;
+      using size_type  = std::size_t;
+      using value_type = Elem;
+
+
+      HashMapFixed() : hasher{}, table{nullptr}, elemV{nullptr}, elemC{0} {}
+      ~HashMapFixed() {free();}
+
+      //
+      // alloc
+      //
+      void alloc(size_type count)
+      {
+         if(elemV) free();
+
+         if(!count) return;
+
+         size_type sizeRaw = sizeof(Elem) * count + sizeof(Elem *) * count;
+
+         elemC = count;
+         elemV = static_cast<Elem *>(::operator new(sizeRaw));
+      }
+
+      // begin
+      iterator begin() {return elemV;}
+
+      //
+      // build
+      //
+      void build()
+      {
+         // Initialize table.
+         table = reinterpret_cast<Elem **>(elemV + elemC);
+         for(Elem **elem = table + elemC; elem != table;)
+            *--elem = nullptr;
+
+         // Insert elements.
+         for(Elem &elem : *this)
+         {
+            size_type hash = hasher(elem.key) % elemC;
+
+            elem.next = table[hash];
+            table[hash] = &elem;
+         }
+      }
+
+      // empty
+      bool empty() const {return !elemC;}
+
+      // end
+      iterator end() {return elemV + elemC;}
+
+      //
+      // find
+      //
+      T *find(Key const &key)
+      {
+         if(!table) return nullptr;
+
+         for(Elem *elem = table[hasher(key) % elemC]; elem; elem = elem->next)
+         {
+            if(elem->key == key)
+               return &elem->val;
+         }
+
+         return nullptr;
+      }
+
+      //
+      // free
+      //
+      void free()
+      {
+         if(table)
+         {
+            for(Elem *elem = elemV + elemC; elem != elemV;)
+               (--elem)->~Elem();
+
+            table = nullptr;
+         }
+
+         ::operator delete(elemV);
+
+         elemV = nullptr;
+         elemC = 0;
+      }
+
+      // size
+      size_type size() const {return elemC;}
+
+   private:
+      Hash hasher;
+
+      Elem    **table;
+      Elem     *elemV;
+      size_type elemC;
+   };
+}
+
+#endif//ACSVM__HashMapFixed_H__
+
diff --git a/src/acs/vm/ACSVM/ID.hpp b/src/acs/vm/ACSVM/ID.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..fbb113cce0e457519b7be27ac3b749d43257e90d
--- /dev/null
+++ b/src/acs/vm/ACSVM/ID.hpp
@@ -0,0 +1,50 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2017 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// Numeric identifiers.
+//
+//-----------------------------------------------------------------------------
+
+#ifndef ACSVM__ID_H__
+#define ACSVM__ID_H__
+
+#include "Types.hpp"
+
+
+//----------------------------------------------------------------------------|
+// Extern Functions                                                           |
+//
+
+namespace ACSVM
+{
+   constexpr std::uint32_t MakeID(char c0, char c1, char c2, char c3);
+   constexpr std::uint32_t MakeID(char const (&s)[5]);
+
+   //
+   // MakeID
+   //
+   constexpr std::uint32_t MakeID(char c0, char c1, char c2, char c3)
+   {
+      return
+         (static_cast<std::uint32_t>(c0) <<  0) |
+         (static_cast<std::uint32_t>(c1) <<  8) |
+         (static_cast<std::uint32_t>(c2) << 16) |
+         (static_cast<std::uint32_t>(c3) << 24);
+   }
+
+   //
+   // MakeID
+   //
+   constexpr std::uint32_t MakeID(char const (&s)[5])
+   {
+      return MakeID(s[0], s[1], s[2], s[3]);
+   }
+}
+
+#endif//ACSVM__ID_H__
+
diff --git a/src/acs/vm/ACSVM/Init.cpp b/src/acs/vm/ACSVM/Init.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..66e65bc8113d0b0701e1567aa5d8026ea621599f
--- /dev/null
+++ b/src/acs/vm/ACSVM/Init.cpp
@@ -0,0 +1,149 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2015 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// Initializer handling.
+//
+//-----------------------------------------------------------------------------
+
+#include "Init.hpp"
+
+#include "Array.hpp"
+#include "Function.hpp"
+#include "Module.hpp"
+#include "String.hpp"
+
+#include <vector>
+
+
+//----------------------------------------------------------------------------|
+// Types                                                                      |
+//
+
+namespace ACSVM
+{
+   //
+   // ArrayInit::PrivData
+   //
+   struct ArrayInit::PrivData
+   {
+      std::vector<WordInit> initV;
+   };
+}
+
+
+//----------------------------------------------------------------------------|
+// Extern Functions                                                           |
+//
+
+namespace ACSVM
+{
+
+   //
+   // ArrayInit constructor
+   //
+   ArrayInit::ArrayInit() :
+      pd{new PrivData}
+   {
+   }
+
+   //
+   // ArrayInit destructor
+   //
+   ArrayInit::~ArrayInit()
+   {
+      delete pd;
+   }
+
+   //
+   // ArrayInit::apply
+   //
+   void ArrayInit::apply(Array &arr, Module *module)
+   {
+      Word idx = 0;
+      for(WordInit &init : pd->initV)
+      {
+         Word value = init.getValue(module);
+         if(value) arr[idx] = value;
+         ++idx;
+      }
+   }
+
+   //
+   // ArrayInit::finish
+   //
+   void ArrayInit::finish()
+   {
+      // Clear out trailing zeroes.
+      while(!pd->initV.empty() && !pd->initV.back())
+         pd->initV.pop_back();
+
+      // Shrink vector.
+      pd->initV.shrink_to_fit();
+
+      // TODO: Break up initialization data into nonzero ranges.
+   }
+
+   //
+   // ArrayInit::reserve
+   //
+   void ArrayInit::reserve(Word count)
+   {
+      pd->initV.resize(count, 0);
+   }
+
+   //
+   // ArrayInit::setTag
+   //
+   void ArrayInit::setTag(Word idx, InitTag tag)
+   {
+      if(idx >= pd->initV.size())
+         pd->initV.resize(idx + 1, 0);
+
+      pd->initV[idx].tag = tag;
+   }
+
+   //
+   // ArrayInit::setVal
+   //
+   void ArrayInit::setVal(Word idx, Word val)
+   {
+      if(idx >= pd->initV.size())
+         pd->initV.resize(idx + 1, 0);
+
+      pd->initV[idx].val = val;
+   }
+
+   //
+   // WordInit::getValue
+   //
+   Word WordInit::getValue(Module *module) const
+   {
+      switch(tag)
+      {
+      case InitTag::Integer:
+         return val;
+
+      case InitTag::Function:
+         if(val < module->functionV.size() && module->functionV[val])
+            return module->functionV[val]->idx;
+         else
+            return val;
+
+      case InitTag::String:
+         if(val < module->stringV.size())
+            return ~module->stringV[val]->idx;
+         else
+            return val;
+      }
+
+      return val;
+   }
+}
+
+// EOF
+
diff --git a/src/acs/vm/ACSVM/Init.hpp b/src/acs/vm/ACSVM/Init.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..aecbf700b9673b807173ac088d2ff24c60d2599d
--- /dev/null
+++ b/src/acs/vm/ACSVM/Init.hpp
@@ -0,0 +1,80 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2015 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// Initializer handling.
+//
+//-----------------------------------------------------------------------------
+
+#ifndef ACSVM__Init_H__
+#define ACSVM__Init_H__
+
+#include "Types.hpp"
+
+
+//----------------------------------------------------------------------------|
+// Types                                                                      |
+//
+
+namespace ACSVM
+{
+   //
+   // InitTag
+   //
+   enum class InitTag
+   {
+      Integer,
+      Function,
+      String,
+   };
+
+   //
+   // ArrayInit
+   //
+   class ArrayInit
+   {
+   public:
+      ArrayInit();
+      ~ArrayInit();
+
+      void apply(Array &arr, Module *module);
+
+      void finish();
+
+      void reserve(Word count);
+
+      void setTag(Word idx, InitTag tag);
+      void setVal(Word idx, Word    val);
+
+   private:
+      struct PrivData;
+
+      PrivData *pd;
+   };
+
+   //
+   // WordInit
+   //
+   class WordInit
+   {
+   public:
+      WordInit() = default;
+      WordInit(Word val_) : val{val_}, tag{InitTag::Integer} {}
+      WordInit(Word val_, InitTag tag_) : val{val_}, tag{tag_} {}
+
+      explicit operator bool () const
+         {return val || tag != InitTag::Integer;}
+
+      Word getValue(Module *module) const;
+
+      Word    val;
+      InitTag tag;
+   };
+}
+
+#endif//ACSVM__Init_H__
+
diff --git a/src/acs/vm/ACSVM/Jump.cpp b/src/acs/vm/ACSVM/Jump.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..fa6efed3789239195369ed473fa416290a4e302c
--- /dev/null
+++ b/src/acs/vm/ACSVM/Jump.cpp
@@ -0,0 +1,44 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2015 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// Jump class.
+//
+//-----------------------------------------------------------------------------
+
+#include "Jump.hpp"
+
+#include "BinaryIO.hpp"
+
+
+//----------------------------------------------------------------------------|
+// Extern Functions                                                           |
+//
+
+namespace ACSVM
+{
+   //
+   // JumpMap::loadJumps
+   //
+   void JumpMap::loadJumps(Byte const *data, std::size_t count)
+   {
+      table.alloc(count);
+      std::size_t iter = 0;
+
+      for(auto &jump : table)
+      {
+         Word caseVal = ReadLE4(data + iter); iter += 4;
+         Word codeIdx = ReadLE4(data + iter); iter += 4;
+         new(&jump) HashMapFixed<Word, Word>::Elem{caseVal, codeIdx, nullptr};
+      }
+
+      table.build();
+   }
+}
+
+// EOF
+
diff --git a/src/acs/vm/ACSVM/Jump.hpp b/src/acs/vm/ACSVM/Jump.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..1656de037aa2952cda4eeff771334854d7624502
--- /dev/null
+++ b/src/acs/vm/ACSVM/Jump.hpp
@@ -0,0 +1,49 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2015 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// Jump class.
+//
+//-----------------------------------------------------------------------------
+
+#ifndef ACSVM__Jump_H__
+#define ACSVM__Jump_H__
+
+#include "HashMapFixed.hpp"
+
+
+//----------------------------------------------------------------------------|
+// Types                                                                      |
+//
+
+namespace ACSVM
+{
+   //
+   // Jump
+   //
+   // Dynamic jump target.
+   //
+   class Jump
+   {
+   public:
+      Word codeIdx;
+   };
+
+   //
+   // JumpMap
+   //
+   class JumpMap
+   {
+   public:
+      void loadJumps(Byte const *data, std::size_t count);
+
+      HashMapFixed<Word, Word> table;
+   };
+}
+
+#endif//ACSVM__Jump_H__
+
diff --git a/src/acs/vm/ACSVM/List.hpp b/src/acs/vm/ACSVM/List.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..3c65a2a350a9cb91b4b0f8bffac1038ebd0fb6c0
--- /dev/null
+++ b/src/acs/vm/ACSVM/List.hpp
@@ -0,0 +1,114 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2015 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// Linked list handling.
+//
+//-----------------------------------------------------------------------------
+
+#ifndef ACSVM__List_H__
+#define ACSVM__List_H__
+
+#include "Types.hpp"
+
+
+//----------------------------------------------------------------------------|
+// Types                                                                      |
+//
+
+namespace ACSVM
+{
+   //
+   // ListLink
+   //
+   template<typename T>
+   class ListLink
+   {
+   private:
+      //
+      // IteratorBase
+      //
+      template<typename Obj>
+      class IteratorBase
+      {
+      public:
+         IteratorBase<Obj> &operator ++ () {link = link->next; return *this;}
+         IteratorBase<Obj> operator ++ (int) {auto i = *this; ++*this; return i;}
+
+         Obj &operator * () const {return *link->obj;}
+         Obj *operator -> () const {return link->obj;}
+
+         bool operator == (IteratorBase<Obj> const &iter) const
+            {return iter.link == link;}
+         bool operator != (IteratorBase<Obj> const &iter) const
+            {return iter.link != link;}
+
+
+         friend class ListLink;
+
+      private:
+         IteratorBase(ListLink<T> const *link_) : link{link_} {}
+
+         ListLink<T> const *link;
+      };
+
+   public:
+      ListLink() : obj{nullptr}, prev{this}, next{this} {}
+      ListLink(ListLink<T> const &) = delete;
+      ListLink(T *obj_) : obj{obj_}, prev{this}, next{this} {}
+      ListLink(T *obj_, ListLink<T> &&link) :
+         obj{obj_}, prev{link.prev}, next{link.next}
+         {prev->next = next->prev = this; link.prev = link.next = &link;}
+      ~ListLink() {unlink();}
+
+      // begin
+      IteratorBase<T>       begin()       {return next;}
+      IteratorBase<T const> begin() const {return next;}
+
+      // end
+      IteratorBase<T>       end()       {return this;}
+      IteratorBase<T const> end() const {return this;}
+
+      //
+      // insert
+      //
+      void insert(ListLink<T> *head)
+      {
+         (prev = head->prev)->next = this;
+         (next = head      )->prev = this;
+      }
+
+      void relink(ListLink<T> *head) {unlink(); insert(head);}
+
+      //
+      // size
+      //
+      std::size_t size() const
+      {
+         std::size_t count = 0;
+         for(auto const &o : *this) (void)o, ++count;
+         return count;
+      }
+
+      //
+      // unlink
+      //
+      void unlink()
+      {
+         prev->next = next;
+         next->prev = prev;
+
+         prev = next = this;
+      }
+
+      T *const obj;
+      ListLink<T> *prev, *next;
+   };
+}
+
+#endif//ACSVM__List_H__
+
diff --git a/src/acs/vm/ACSVM/Module.cpp b/src/acs/vm/ACSVM/Module.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..95626be7b5ac878c268d346e2704e7d752cc34e7
--- /dev/null
+++ b/src/acs/vm/ACSVM/Module.cpp
@@ -0,0 +1,139 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2015 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// Module class.
+//
+//-----------------------------------------------------------------------------
+
+#include "Module.hpp"
+
+#include "Array.hpp"
+#include "Environment.hpp"
+#include "Function.hpp"
+#include "Init.hpp"
+#include "Jump.hpp"
+#include "Script.hpp"
+
+
+//----------------------------------------------------------------------------|
+// Extern Functions                                                           |
+//
+
+namespace ACSVM
+{
+   //
+   // ModuleName::hash
+   //
+   std::size_t ModuleName::hash() const
+   {
+      return s->hash + std::hash<void*>()(p) + i;
+   }
+
+   //
+   // Module constructor
+   //
+   Module::Module(Environment *env_, ModuleName const &name_) :
+      env{env_},
+      name{name_},
+
+      hashLink{this},
+
+      isACS0{false},
+      loaded{false}
+   {
+   }
+
+   //
+   // Module destructor
+   //
+   Module::~Module()
+   {
+      reset();
+   }
+
+   //
+   // Module::refStrings
+   //
+   void Module::refStrings() const
+   {
+      if(name.s) name.s->ref = true;
+
+      for(auto &s : arrImpV)   if(s) s->ref = true;
+      for(auto &s : arrNameV)  if(s) s->ref = true;
+      for(auto &s : funcNameV) if(s) s->ref = true;
+      for(auto &s : regImpV)   if(s) s->ref = true;
+      for(auto &s : regNameV)  if(s) s->ref = true;
+      for(auto &s : scrNameV)  if(s) s->ref = true;
+      for(auto &s : stringV)   if(s) s->ref = true;
+
+      for(auto &func : functionV)
+         if(func && func->name) func->name->ref = true;
+
+      for(auto &scr : scriptV)
+         if(scr.name.s) scr.name.s->ref = true;
+   }
+
+   //
+   // Module::reset
+   //
+   void Module::reset()
+   {
+      // Unload locally defined functions from env.
+      for(Function *&func : functionV)
+      {
+         if(func && func->module == this)
+            env->freeFunction(func);
+      }
+
+      arrImpV.free();
+      arrInitV.free();
+      arrNameV.free();
+      arrSizeV.free();
+      codeV.free();
+      funcNameV.free();
+      functionV.free();
+      importV.free();
+      jumpV.free();
+      jumpMapV.free();
+      regImpV.free();
+      regInitV.free();
+      regNameV.free();
+      scrNameV.free();
+      scriptV.free();
+      stringV.free();
+
+      isACS0 = false;
+      loaded = false;
+   }
+
+   //
+   // Module::resetStrings
+   //
+   void Module::resetStrings()
+   {
+      name.s = env->getString(name.s);
+
+      for(auto &s : arrImpV)   s = env->getString(s);
+      for(auto &s : arrNameV)  s = env->getString(s);
+      for(auto &s : funcNameV) s = env->getString(s);
+      for(auto &s : regImpV)   s = env->getString(s);
+      for(auto &s : regNameV)  s = env->getString(s);
+      for(auto &s : scrNameV)  s = env->getString(s);
+      for(auto &s : stringV)   s = env->getString(s);
+
+      for(auto &func : functionV)
+         if(func) func->name = env->getString(func->name);
+
+      for(auto &scr : scriptV)
+         scr.name.s = env->getString(scr.name.s);
+   }
+}
+
+
+// EOF
+
diff --git a/src/acs/vm/ACSVM/Module.hpp b/src/acs/vm/ACSVM/Module.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..b2b46e6132443842ea8d9165f9a76e9d9378e305
--- /dev/null
+++ b/src/acs/vm/ACSVM/Module.hpp
@@ -0,0 +1,178 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2015-2017 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// Module class.
+//
+//-----------------------------------------------------------------------------
+
+#ifndef ACSVM__Module_H__
+#define ACSVM__Module_H__
+
+#include "ID.hpp"
+#include "List.hpp"
+#include "Vector.hpp"
+
+#include <functional>
+#include <memory>
+
+
+//----------------------------------------------------------------------------|
+// Types                                                                      |
+//
+
+namespace ACSVM
+{
+   //
+   // ModuleName
+   //
+   // Stores a Module's name. Name semantics are user-defined and must provide
+   // a (user-defined) mapping from name to bytecode data. The names are used
+   // internally only for determining if a specific module has already been
+   // loaded. That is, two ModuleNames should compare equal if and only if they
+   // designate the same bytecode data.
+   //
+   class ModuleName
+   {
+   public:
+      ModuleName(String *s_, void *p_, std::size_t i_) : s{s_}, p{p_}, i{i_} {}
+
+      bool operator == (ModuleName const &name) const
+         {return s == name.s && p == name.p && i == name.i;}
+      bool operator != (ModuleName const &name) const
+         {return s != name.s || p != name.p || i != name.i;}
+
+      std::size_t hash() const;
+
+      // String value. May be null.
+      String *s;
+
+      // Arbitrary pointer value.
+      void *p;
+
+      // Arbitrary integer value.
+      std::size_t i;
+   };
+
+   //
+   // Module
+   //
+   // Represents an ACS bytecode module.
+   //
+   class Module
+   {
+   public:
+      Module(Environment *env, ModuleName const &name);
+      ~Module();
+
+      void readBytecode(Byte const *data, std::size_t size);
+
+      void refStrings() const;
+
+      void reset();
+
+      void resetStrings();
+
+      Environment *env;
+      ModuleName   name;
+
+      Vector<String *>   arrImpV;
+      Vector<ArrayInit>  arrInitV;
+      Vector<String *>   arrNameV;
+      Vector<Word>       arrSizeV;
+      Vector<Word>       codeV;
+      Vector<String *>   funcNameV;
+      Vector<Function *> functionV;
+      Vector<Module *>   importV;
+      Vector<Jump>       jumpV;
+      Vector<JumpMap>    jumpMapV;
+      Vector<String *>   regImpV;
+      Vector<WordInit>   regInitV;
+      Vector<String *>   regNameV;
+      Vector<String *>   scrNameV;
+      Vector<Script>     scriptV;
+      Vector<String *>   stringV;
+
+      ListLink<Module> hashLink;
+
+      bool isACS0;
+      bool loaded;
+
+
+      static std::pair<
+         std::unique_ptr<Byte[]> /*data*/,
+         std::size_t             /*size*/>
+      DecryptStringACSE(Byte const *data, std::size_t size, std::size_t iter);
+
+      static std::unique_ptr<char[]> ParseStringACS0(Byte const *first,
+         Byte const *last, std::size_t len);
+
+      static std::tuple<
+         Byte const * /*begin*/,
+         Byte const * /*end*/,
+         std::size_t  /*len*/>
+      ScanStringACS0(Byte const *data, std::size_t size, std::size_t iter);
+
+   private:
+      bool chunkIterACSE(Byte const *data, std::size_t size,
+         bool (Module::*chunker)(Byte const *, std::size_t, Word));
+
+      void chunkStrTabACSE(Vector<String *> &strV,
+         Byte const *data, std::size_t size, bool junk);
+
+      bool chunkerACSE_AIMP(Byte const *data, std::size_t size, Word chunkName);
+      bool chunkerACSE_AINI(Byte const *data, std::size_t size, Word chunkName);
+      bool chunkerACSE_ARAY(Byte const *data, std::size_t size, Word chunkName);
+      bool chunkerACSE_ASTR(Byte const *data, std::size_t size, Word chunkName);
+      bool chunkerACSE_ATAG(Byte const *data, std::size_t size, Word chunkName);
+      bool chunkerACSE_FARY(Byte const *data, std::size_t size, Word chunkName);
+      bool chunkerACSE_FNAM(Byte const *data, std::size_t size, Word chunkName);
+      bool chunkerACSE_FUNC(Byte const *data, std::size_t size, Word chunkName);
+      bool chunkerACSE_JUMP(Byte const *data, std::size_t size, Word chunkName);
+      bool chunkerACSE_LOAD(Byte const *data, std::size_t size, Word chunkName);
+      bool chunkerACSE_MEXP(Byte const *data, std::size_t size, Word chunkName);
+      bool chunkerACSE_MIMP(Byte const *data, std::size_t size, Word chunkName);
+      bool chunkerACSE_MINI(Byte const *data, std::size_t size, Word chunkName);
+      bool chunkerACSE_MSTR(Byte const *data, std::size_t size, Word chunkName);
+      bool chunkerACSE_SARY(Byte const *data, std::size_t size, Word chunkName);
+      bool chunkerACSE_SFLG(Byte const *data, std::size_t size, Word chunkName);
+      bool chunkerACSE_SNAM(Byte const *data, std::size_t size, Word chunkName);
+      bool chunkerACSE_SPTR8(Byte const *data, std::size_t size, Word chunkName);
+      bool chunkerACSE_SPTR12(Byte const *data, std::size_t size, Word chunkName);
+      bool chunkerACSE_STRE(Byte const *data, std::size_t size, Word chunkName);
+      bool chunkerACSE_STRL(Byte const *data, std::size_t size, Word chunkName);
+      bool chunkerACSE_SVCT(Byte const *data, std::size_t size, Word chunkName);
+
+      void readBytecodeACS0(Byte const *data, std::size_t size);
+      void readBytecodeACSE(Byte const *data, std::size_t size,
+         bool compressed, std::size_t iter = 4);
+
+      void readChunksACSE(Byte const *data, std::size_t size, bool fakeACS0);
+
+      void readCodeACS0(Byte const *data, std::size_t size, bool compressed);
+
+      String *readStringACS0(Byte const *data, std::size_t size, std::size_t iter);
+
+      void setScriptNameTypeACSE(Script *scr, Word nameInt, Word type);
+   };
+}
+
+namespace std
+{
+   //
+   // hash<::ACSVM::ModuleName>
+   //
+   template<>
+   struct hash<::ACSVM::ModuleName>
+   {
+      size_t operator () (::ACSVM::ModuleName const &name) const
+         {return name.hash();}
+   };
+}
+
+#endif//ACSVM__Module_H__
+
diff --git a/src/acs/vm/ACSVM/ModuleACS0.cpp b/src/acs/vm/ACSVM/ModuleACS0.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..7ae16ac2f2efd1ce0bd4d16b1a8a3f4342984907
--- /dev/null
+++ b/src/acs/vm/ACSVM/ModuleACS0.cpp
@@ -0,0 +1,333 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2015-2017 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// Module class bytecode reading.
+//
+//-----------------------------------------------------------------------------
+
+#include "Module.hpp"
+
+#include "BinaryIO.hpp"
+#include "Environment.hpp"
+#include "Error.hpp"
+#include "Jump.hpp"
+#include "Script.hpp"
+#include "Tracer.hpp"
+
+
+//----------------------------------------------------------------------------|
+// Extern Functions                                                           |
+//
+
+namespace ACSVM
+{
+   //
+   // Module::reaBytecode
+   //
+   void Module::readBytecode(Byte const *data, std::size_t size)
+   {
+      try
+      {
+         if(size < 4) throw ReadError();
+
+         switch(ReadLE4(data))
+         {
+         case MakeID("ACS\0"):
+            readBytecodeACS0(data, size);
+            break;
+
+         case MakeID("ACSE"):
+            readBytecodeACSE(data, size, false);
+            break;
+
+         case MakeID("ACSe"):
+            readBytecodeACSE(data, size, true);
+            break;
+         }
+      }
+      catch(...)
+      {
+         // If an exception occurs before module is fully loaded, reset it.
+         if(!loaded)
+            reset();
+
+         throw;
+      }
+   }
+
+   //
+   // Module::readBytecodeACS0
+   //
+   void Module::readBytecodeACS0(Byte const *data, std::size_t size)
+   {
+      std::size_t iter;
+
+      // Read table index.
+      if(size < 8) throw ReadError();
+      iter = ReadLE4(data + 4);
+      if(iter > size) throw ReadError();
+
+      // Check for ACSE header behind indicated table.
+      if(iter >= 8)
+      {
+         switch(ReadLE4(data + (iter - 4)))
+         {
+         case MakeID("ACSE"):
+            return readBytecodeACSE(data, size, false, iter - 8);
+
+         case MakeID("ACSe"):
+            return readBytecodeACSE(data, size, true, iter - 8);
+         }
+      }
+
+      // Mark as ACS0.
+      isACS0 = true;
+
+      // Read script table.
+
+      // Read script count.
+      if(size - iter < 4) throw ReadError();
+      scriptV.alloc(ReadLE4(data + iter), this); iter += 4;
+
+      // Read scripts.
+      if(size - iter < scriptV.size() * 12) throw ReadError();
+      for(Script &scr : scriptV)
+      {
+         scr.name.i  = ReadLE4(data + iter); iter += 4;
+         scr.codeIdx = ReadLE4(data + iter); iter += 4;
+         scr.argC    = ReadLE4(data + iter); iter += 4;
+
+         std::tie(scr.type, scr.name.i) = env->getScriptTypeACS0(scr.name.i);
+      }
+
+      // Read string table.
+
+      // Read string count.
+      if(size - iter < 4) throw ReadError();
+      stringV.alloc(ReadLE4(data + iter)); iter += 4;
+
+      // Read strings.
+      if(size - iter < stringV.size() * 4) throw ReadError();
+      for(String *&str : stringV)
+      {
+         str = readStringACS0(data, size, ReadLE4(data + iter)); iter += 4;
+      }
+
+      // Read code.
+      readCodeACS0(data, size, false);
+
+      loaded = true;
+   }
+
+   //
+   // Module::readCodeACS0
+   //
+   void Module::readCodeACS0(Byte const *data, std::size_t size, bool compressed)
+   {
+      TracerACS0 tracer{env, data, size, compressed};
+
+      // Trace code paths from this module.
+      tracer.trace(this);
+
+      codeV.alloc(tracer.codeC);
+      jumpMapV.alloc(tracer.jumpMapC);
+
+      tracer.translate(this);
+   }
+
+   //
+   // Module::readStringACS0
+   //
+   String *Module::readStringACS0(Byte const *data, std::size_t size, std::size_t iter)
+   {
+      Byte const *begin, *end;
+      std::size_t len;
+      std::tie(begin, end, len) = ScanStringACS0(data, size, iter);
+
+      // If result length is same as input length, no processing is needed.
+      if(static_cast<std::size_t>(end - begin) == len)
+      {
+         // Byte is always unsigned char, which is allowed to alias with char.
+         return env->getString(
+            reinterpret_cast<char const *>(begin),
+            reinterpret_cast<char const *>(end));
+      }
+      else
+         return env->getString(ParseStringACS0(begin, end, len).get(), len);
+   }
+
+   //
+   // Module::ParseStringACS0
+   //
+   std::unique_ptr<char[]> Module::ParseStringACS0(Byte const *first,
+      Byte const *last, std::size_t len)
+   {
+      std::unique_ptr<char[]> buf{new char[len + 1]};
+      char                   *bufItr = buf.get();
+
+      for(Byte const *s = first; s != last;)
+      {
+         if(*s == '\\')
+         {
+            if(++s == last)
+               break;
+
+            switch(*s)
+            {
+            case 'a': *bufItr++ += '\a';   ++s; break;
+            case 'b': *bufItr++ += '\b';   ++s; break;
+            case 'c': *bufItr++ += '\x1C'; ++s; break; // ZDoom color escape
+            case 'f': *bufItr++ += '\f';   ++s; break;
+            case 'r': *bufItr++ += '\r';   ++s; break;
+            case 'n': *bufItr++ += '\n';   ++s; break;
+            case 't': *bufItr++ += '\t';   ++s; break;
+            case 'v': *bufItr++ += '\v';   ++s; break;
+
+            case '0': case '1': case '2': case '3':
+            case '4': case '5': case '6': case '7':
+               for(unsigned int i = 3, c = 0; i-- && s != last; ++s)
+               {
+                  switch(*s)
+                  {
+                  case '0': c = c * 8 + 00; continue;
+                  case '1': c = c * 8 + 01; continue;
+                  case '2': c = c * 8 + 02; continue;
+                  case '3': c = c * 8 + 03; continue;
+                  case '4': c = c * 8 + 04; continue;
+                  case '5': c = c * 8 + 05; continue;
+                  case '6': c = c * 8 + 06; continue;
+                  case '7': c = c * 8 + 07; continue;
+                  }
+
+                  *bufItr++ = c;
+                  break;
+               }
+               break;
+
+            case 'X': case 'x':
+               ++s;
+               for(unsigned int i = 2, c = 0; i-- && s != last; ++s)
+               {
+                  switch(*s)
+                  {
+                  case '0': c = c * 16 + 0x0; continue;
+                  case '1': c = c * 16 + 0x1; continue;
+                  case '2': c = c * 16 + 0x2; continue;
+                  case '3': c = c * 16 + 0x3; continue;
+                  case '4': c = c * 16 + 0x4; continue;
+                  case '5': c = c * 16 + 0x5; continue;
+                  case '6': c = c * 16 + 0x6; continue;
+                  case '7': c = c * 16 + 0x7; continue;
+                  case '8': c = c * 16 + 0x8; continue;
+                  case '9': c = c * 16 + 0x9; continue;
+                  case 'A': c = c * 16 + 0xA; continue;
+                  case 'B': c = c * 16 + 0xB; continue;
+                  case 'C': c = c * 16 + 0xC; continue;
+                  case 'D': c = c * 16 + 0xD; continue;
+                  case 'E': c = c * 16 + 0xE; continue;
+                  case 'F': c = c * 16 + 0xF; continue;
+                  case 'a': c = c * 16 + 0xa; continue;
+                  case 'b': c = c * 16 + 0xb; continue;
+                  case 'c': c = c * 16 + 0xc; continue;
+                  case 'd': c = c * 16 + 0xd; continue;
+                  case 'e': c = c * 16 + 0xe; continue;
+                  case 'f': c = c * 16 + 0xf; continue;
+                  }
+
+                  *bufItr++ = c;
+                  break;
+               }
+               break;
+
+            default:
+               *bufItr++ = *s++;
+               break;
+            }
+         }
+         else
+            *bufItr++ = *s++;
+      }
+
+      *bufItr++ = '\0';
+
+      return buf;
+   }
+
+   //
+   // Module::ScanStringACS0
+   //
+   std::tuple<
+      Byte const * /*begin*/,
+      Byte const * /*end*/,
+      std::size_t  /*len*/>
+   Module::ScanStringACS0(Byte const *data, std::size_t size, std::size_t iter)
+   {
+      if(iter > size) throw ReadError();
+
+      Byte const *begin = data + iter;
+      Byte const *end   = data + size;
+      Byte const *s     = begin;
+      std::size_t len   = 0;
+
+      while(s != end && *s)
+      {
+         if(*s++ == '\\')
+         {
+            if(s == end || !*s)
+               break;
+
+            switch(*s++)
+            {
+            case '0': case '1': case '2': case '3':
+            case '4': case '5': case '6': case '7':
+               for(int i = 2; i-- && s != end; ++s)
+               {
+                  switch(*s)
+                  {
+                  case '0': case '1': case '2': case '3':
+                  case '4': case '5': case '6': case '7':
+                     continue;
+                  }
+
+                  break;
+               }
+               break;
+
+            case 'X': case 'x':
+               for(int i = 2; i-- && s != end; ++s)
+               {
+                  switch(*s)
+                  {
+                  case '0': case '1': case '2': case '3': case '4':
+                  case '5': case '6': case '7': case '8': case '9':
+                  case 'A': case 'B': case 'C': case 'D': case 'E': case 'F':
+                  case 'a': case 'b': case 'c': case 'd': case 'e': case 'f':
+                     continue;
+                  }
+
+                  break;
+               }
+               break;
+
+            default:
+               break;
+            }
+         }
+
+         ++len;
+      }
+
+      // If not terminated by a null, string is malformed.
+      if(s == end) throw ReadError();
+
+      return std::make_tuple(begin, s, len);
+   }
+}
+
+// EOF
+
diff --git a/src/acs/vm/ACSVM/ModuleACSE.cpp b/src/acs/vm/ACSVM/ModuleACSE.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..a38a29f5be3f26aa8652f1d6fdfedd7c997883e1
--- /dev/null
+++ b/src/acs/vm/ACSVM/ModuleACSE.cpp
@@ -0,0 +1,860 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2015-2017 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// Module class bytecode reading.
+//
+//-----------------------------------------------------------------------------
+
+#include "Module.hpp"
+
+#include "Array.hpp"
+#include "BinaryIO.hpp"
+#include "Environment.hpp"
+#include "Error.hpp"
+#include "Function.hpp"
+#include "Init.hpp"
+#include "Jump.hpp"
+#include "Script.hpp"
+
+#include <algorithm>
+
+
+//----------------------------------------------------------------------------|
+// Extern Functions                                                           |
+//
+
+namespace ACSVM
+{
+   //
+   // Module::chunkIterACSE
+   //
+   bool Module::chunkIterACSE(Byte const *data, std::size_t size,
+      bool (Module::*chunker)(Byte const *, std::size_t, Word))
+   {
+      std::size_t iter = 0;
+
+      while(iter != size)
+      {
+         // Need space for header.
+         if(size - iter < 8) throw ReadError();
+
+         // Read header.
+         Word chunkName = ReadLE4(data + iter + 0);
+         Word chunkSize = ReadLE4(data + iter + 4);
+
+         // Consume header.
+         iter += 8;
+
+         // Need space for payload.
+         if(size - iter < chunkSize) throw ReadError();
+
+         // Read payload.
+         if((this->*chunker)(data + iter, chunkSize, chunkName))
+            return true;
+
+         // Consume payload.
+         iter += chunkSize;
+      }
+
+      return false;
+   }
+
+   //
+   // Module::chunkStrTabACSE
+   //
+   void Module::chunkStrTabACSE(Vector<String *> &strV,
+      Byte const *data, std::size_t size, bool junk)
+   {
+      std::size_t iter = 0;
+
+      if(junk)
+      {
+         if(size < 12) throw ReadError();
+
+         /*junk   = ReadLE4(data + iter);*/ iter += 4;
+         strV.alloc(ReadLE4(data + iter));  iter += 4;
+         /*junk   = ReadLE4(data + iter);*/ iter += 4;
+      }
+      else
+      {
+         if(size < 4) throw ReadError();
+
+         strV.alloc(ReadLE4(data + iter)); iter += 4;
+      }
+
+      if(size - iter < strV.size() * 4) throw ReadError();
+      for(String *&str : strV)
+      {
+         str = readStringACS0(data, size, ReadLE4(data + iter)); iter += 4;
+      }
+   }
+
+   //
+   // Module::chunkerACSE_AIMP
+   //
+   bool Module::chunkerACSE_AIMP(Byte const *data, std::size_t size, Word chunkName)
+   {
+      if(chunkName != MakeID("AIMP")) return false;
+
+      if(size < 4) throw ReadError();
+
+      // Chunk starts with a number of entries. However, that is redundant with
+      // just checking for the end of the chunk as in MIMP, so do that.
+
+      // Determine highest index.
+      Word arrC = 0;
+      for(std::size_t iter = 4; iter != size;)
+      {
+         if(size - iter < 8) throw ReadError();
+
+         Word idx = ReadLE4(data + iter);   iter += 4;
+         /*   len = LeadLE4(data + iter);*/ iter += 4;
+
+         arrC = std::max<Word>(arrC, idx + 1);
+
+         Byte const *next;
+         std::tie(std::ignore, next, std::ignore) = ScanStringACS0(data, size, iter);
+
+         iter = next - data + 1;
+      }
+
+      // Read imports.
+      arrImpV.alloc(arrC);
+      for(std::size_t iter = 4; iter != size;)
+      {
+         Word idx = ReadLE4(data + iter);   iter += 4;
+         /*   len = LeadLE4(data + iter);*/ iter += 4;
+
+         Byte const *next;
+         std::size_t len;
+         std::tie(std::ignore, next, len) = ScanStringACS0(data, size, iter);
+
+         std::unique_ptr<char[]> str = ParseStringACS0(data + iter, next, len);
+
+         arrImpV[idx] = env->getString(str.get(), len);
+
+         iter = next - data + 1;
+      }
+
+      return true;
+   }
+
+   //
+   // Module::chunkerACSE_AINI
+   //
+   bool Module::chunkerACSE_AINI(Byte const *data, std::size_t size, Word chunkName)
+   {
+      if(chunkName != MakeID("AINI")) return false;
+
+      if(size < 4 || size % 4) throw ReadError();
+
+      Word idx = ReadLE4(data);
+
+      // Silently ignore out of bounds initializers.
+      if(idx >= arrInitV.size()) return false;
+
+      auto &init = arrInitV[idx];
+      for(std::size_t iter = 4; iter != size; iter += 4)
+         init.setVal(iter / 4 - 1, ReadLE4(data + iter));
+
+      return false;
+   }
+
+   //
+   // Module::chunkerACSE_ARAY
+   //
+   bool Module::chunkerACSE_ARAY(Byte const *data, std::size_t size, Word chunkName)
+   {
+      if(chunkName != MakeID("ARAY")) return false;
+
+      if(size % 8) throw ReadError();
+
+      Word arrC = 0;
+
+      // Determine highest index.
+      for(std::size_t iter = 0; iter != size; iter += 8)
+         arrC = std::max<Word>(arrC, ReadLE4(data + iter) + 1);
+
+      arrNameV.alloc(arrC);
+      arrInitV.alloc(arrC);
+      arrSizeV.alloc(arrC);
+
+      for(std::size_t iter = 0; iter != size;)
+      {
+         Word idx = ReadLE4(data + iter); iter += 4;
+         Word len = ReadLE4(data + iter); iter += 4;
+
+         arrInitV[idx].reserve(len);
+         arrSizeV[idx] = len;
+
+         // Use names from MEXP.
+         if(idx < regNameV.size())
+         {
+            arrNameV[idx] = regNameV[idx];
+            regNameV[idx] = nullptr;
+         }
+      }
+
+      return true;
+   }
+
+   //
+   // Module::chunkerACSE_ASTR
+   //
+   bool Module::chunkerACSE_ASTR(Byte const *data, std::size_t size, Word chunkName)
+   {
+      if(chunkName != MakeID("ASTR")) return false;
+
+      if(size % 4) throw ReadError();
+
+      for(std::size_t iter = 0; iter != size;)
+      {
+         Word idx = ReadLE4(data + iter); iter += 4;
+
+         // Silently ignore out of bounds initializers.
+         if(idx >= arrInitV.size()) continue;
+
+         auto &init = arrInitV[idx];
+         for(Word i = 0, e = arrSizeV[idx]; i != e; ++i)
+            init.setTag(i, InitTag::String);
+      }
+
+      return false;
+   }
+
+   //
+   // Module::chunkerACSE_ATAG
+   //
+   bool Module::chunkerACSE_ATAG(Byte const *data, std::size_t size, Word chunkName)
+   {
+      if(chunkName != MakeID("ATAG")) return false;
+
+      if(size < 5 || data[0]) throw ReadError();
+
+      Word idx = ReadLE4(data + 1);
+
+      // Silently ignore out of bounds initializers.
+      if(idx >= arrInitV.size()) return false;
+
+      auto &init = arrInitV[idx];
+      for(std::size_t iter = 5; iter != size; ++iter)
+      {
+         switch(data[iter])
+         {
+         case 0: init.setTag(iter - 5, InitTag::Integer);  break;
+         case 1: init.setTag(iter - 5, InitTag::String);   break;
+         case 2: init.setTag(iter - 5, InitTag::Function); break;
+         }
+      }
+
+      return false;
+   }
+
+   //
+   // Module::chunkerACSE_FARY
+   //
+   bool Module::chunkerACSE_FARY(Byte const *data, std::size_t size, Word chunkName)
+   {
+      if(chunkName != MakeID("FARY")) return false;
+
+      if(size < 2 || (size - 2) % 4) throw ReadError();
+
+      Word        idx  = ReadLE2(data);
+      std::size_t arrC = (size - 2) / 4;
+
+      if(idx < functionV.size() && functionV[idx])
+         functionV[idx]->locArrC = arrC;
+
+      return false;
+   }
+
+   //
+   // Module::chunkerACSE_FNAM
+   //
+   bool Module::chunkerACSE_FNAM(Byte const *data, std::size_t size, Word chunkName)
+   {
+      if(chunkName != MakeID("FNAM")) return false;
+
+      chunkStrTabACSE(funcNameV, data, size, false);
+
+      return true;
+   }
+
+   //
+   // Module::chunkerACSE_FUNC
+   //
+   bool Module::chunkerACSE_FUNC(Byte const *data, std::size_t size, Word chunkName)
+   {
+      if(chunkName != MakeID("FUNC")) return false;
+
+      if(size % 8) throw ReadError();
+
+      // Read functions.
+      functionV.alloc(size / 8);
+
+      std::size_t iter = 0;
+      for(Function *&func : functionV)
+      {
+         Word idx     = iter / 8;
+         Word argC    = ReadLE1(data + iter); iter += 1;
+         Word locRegC = ReadLE1(data + iter); iter += 1;
+         Word flags   = ReadLE2(data + iter); iter += 2;
+         Word codeIdx = ReadLE4(data + iter); iter += 4;
+
+         // Ignore undefined functions for now.
+         if(!codeIdx) continue;
+
+         String   *funcName = idx < funcNameV.size() ? funcNameV[idx] : nullptr;
+         Function *function = env->getFunction(this, funcName);
+
+         function->argC    = argC;
+         function->locRegC = locRegC;
+         function->flagRet = flags & 0x0001;
+         function->codeIdx = codeIdx;
+
+         func = function;
+      }
+
+      return true;
+   }
+
+   //
+   // Module::chunkerACSE_JUMP
+   //
+   bool Module::chunkerACSE_JUMP(Byte const *data, std::size_t size, Word chunkName)
+   {
+      if(chunkName != MakeID("JUMP")) return false;
+
+      if(size % 4) throw ReadError();
+
+      // Read jumps.
+      jumpV.alloc(size / 4);
+
+      std::size_t iter = 0;
+      for(Jump &jump : jumpV)
+      {
+         jump.codeIdx = ReadLE4(data + iter); iter += 4;
+      }
+
+      return true;
+   }
+
+   //
+   // Module::chunkerACSE_LOAD
+   //
+   bool Module::chunkerACSE_LOAD(Byte const *data, std::size_t size, Word chunkName)
+   {
+      if(chunkName != MakeID("LOAD")) return false;
+
+      // Count imports.
+      std::size_t importC = 0;
+      for(Byte const *iter = data, *end = data + size; iter != end; ++ iter)
+         if(!*iter) ++importC;
+
+      importV.alloc(importC);
+
+      for(std::size_t iter = 0, i = 0; iter != size;)
+      {
+         Byte const *next;
+         std::size_t len;
+         std::tie(std::ignore, next, len) = ScanStringACS0(data, size, iter);
+
+         std::unique_ptr<char[]> str = ParseStringACS0(data + iter, next, len);
+
+         auto loadName = env->getModuleName(str.get(), len);
+         if(loadName != name)
+            importV[i++] = env->getModule(std::move(loadName));
+         else
+            importV[i++] = this;
+
+         iter = next - data + 1;
+      }
+
+      return true;
+   }
+
+   //
+   // Module::chunkerACSE_MEXP
+   //
+   bool Module::chunkerACSE_MEXP(Byte const *data, std::size_t size, Word chunkName)
+   {
+      if(chunkName != MakeID("MEXP")) return false;
+
+      chunkStrTabACSE(regNameV, data, size, false);
+
+      return true;
+   }
+
+   //
+   // Module::chunkerACSE_MIMP
+   //
+   bool Module::chunkerACSE_MIMP(Byte const *data, std::size_t size, Word chunkName)
+   {
+      if(chunkName != MakeID("MIMP")) return false;
+
+      // Determine highest index.
+      Word regC = 0;
+      for(std::size_t iter = 0; iter != size;)
+      {
+         if(size - iter < 4) throw ReadError();
+
+         Word idx = ReadLE4(data + iter); iter += 4;
+
+         regC = std::max<Word>(regC, idx + 1);
+
+         Byte const *next;
+         std::tie(std::ignore, next, std::ignore) = ScanStringACS0(data, size, iter);
+
+         iter = next - data + 1;
+      }
+
+      // Read imports.
+      regImpV.alloc(regC);
+      for(std::size_t iter = 0; iter != size;)
+      {
+         Word idx = ReadLE4(data + iter); iter += 4;
+
+         Byte const *next;
+         std::size_t len;
+         std::tie(std::ignore, next, len) = ScanStringACS0(data, size, iter);
+
+         std::unique_ptr<char[]> str = ParseStringACS0(data + iter, next, len);
+
+         regImpV[idx] = env->getString(str.get(), len);
+
+         iter = next - data + 1;
+      }
+
+      return true;
+   }
+
+   //
+   // Module::chunkerACSE_MINI
+   //
+   bool Module::chunkerACSE_MINI(Byte const *data, std::size_t size, Word chunkName)
+   {
+      if(chunkName != MakeID("MINI")) return false;
+
+      if(size % 4 || size < 4) throw ReadError("bad MINI size");
+
+      Word idx  = ReadLE4(data);
+      Word regC = idx + size / 4 - 1;
+
+      if(regC > regInitV.size())
+         regInitV.realloc(regC);
+
+      for(std::size_t iter = 4; iter != size; iter += 4)
+         regInitV[idx++] = ReadLE4(data + iter);
+
+      return true;
+   }
+
+   //
+   // Module::chunkerACSE_MSTR
+   //
+   bool Module::chunkerACSE_MSTR(Byte const *data, std::size_t size, Word chunkName)
+   {
+      if(chunkName != MakeID("MSTR")) return false;
+
+      if(size % 4) throw ReadError();
+
+      for(std::size_t iter = 0; iter != size;)
+      {
+         Word idx = ReadLE4(data + iter); iter += 4;
+
+         // Silently ignore out of bounds initializers.
+         if(idx < regInitV.size())
+            regInitV[idx].tag = InitTag::String;
+      }
+
+      return false;
+   }
+
+   //
+   // Module::chunkerACSE_SARY
+   //
+   bool Module::chunkerACSE_SARY(Byte const *data, std::size_t size, Word chunkName)
+   {
+      if(chunkName != MakeID("SARY")) return false;
+
+      if(size < 2 || (size - 2) % 4) throw ReadError();
+
+      Word        nameInt = ReadLE2(data);
+      std::size_t arrC    = (size - 2) / 4;
+
+      if(nameInt & 0x8000) nameInt |= 0xFFFF0000;
+
+      for(Script &scr : scriptV)
+         if(scr.name.i == nameInt) scr.locArrC = arrC;
+
+      return false;
+   }
+
+   //
+   // Module::chunkerACSE_SFLG
+   //
+   bool Module::chunkerACSE_SFLG(Byte const *data, std::size_t size, Word chunkName)
+   {
+      if(chunkName != MakeID("SFLG")) return false;
+
+      if(size % 4) throw ReadError();
+
+      for(std::size_t iter = 0; iter != size;)
+      {
+         Word nameInt = ReadLE2(data + iter); iter += 2;
+         Word flags   = ReadLE2(data + iter); iter += 2;
+
+         bool flagNet    = !!(flags & 0x0001);
+         bool flagClient = !!(flags & 0x0002);
+
+         if(nameInt & 0x8000) nameInt |= 0xFFFF0000;
+
+         for(Script &scr : scriptV)
+         {
+            if(scr.name.i == nameInt)
+            {
+               scr.flagClient = flagClient;
+               scr.flagNet    = flagNet;
+            }
+         }
+      }
+
+      return false;
+   }
+
+   //
+   // Module::chunkerACSE_SNAM
+   //
+   bool Module::chunkerACSE_SNAM(Byte const *data, std::size_t size, Word chunkName)
+   {
+      if(chunkName != MakeID("SNAM")) return false;
+
+      chunkStrTabACSE(scrNameV, data, size, false);
+
+      return true;
+   }
+
+   //
+   // Module::chunkerACSE_SPTR8
+   //
+   // Reads 8-byte SPTR chunk.
+   //
+   bool Module::chunkerACSE_SPTR8(Byte const *data, std::size_t size, Word chunkName)
+   {
+      if(chunkName != MakeID("SPTR")) return false;
+
+      if(size % 8) throw ReadError();
+
+      // Read scripts.
+      scriptV.alloc(size / 8, this);
+
+      std::size_t iter = 0;
+      for(Script &scr : scriptV)
+      {
+         Word nameInt = ReadLE2(data + iter); iter += 2;
+         Word type    = ReadLE1(data + iter); iter += 1;
+         scr.argC     = ReadLE1(data + iter); iter += 1;
+         scr.codeIdx  = ReadLE4(data + iter); iter += 4;
+
+         if(nameInt & 0x8000) nameInt |= 0xFFFF0000;
+         setScriptNameTypeACSE(&scr, nameInt, type);
+      }
+
+      return true;
+   }
+
+   //
+   // Module::chunkerACSE_SPTR12
+   //
+   // Reads 12-byte SPTR chunk.
+   //
+   bool Module::chunkerACSE_SPTR12(Byte const *data, std::size_t size, Word chunkName)
+   {
+      if(chunkName != MakeID("SPTR")) return false;
+
+      if(size % 12) throw ReadError();
+
+      // Read scripts.
+      scriptV.alloc(size / 12, this);
+
+      std::size_t iter = 0;
+      for(Script &scr : scriptV)
+      {
+         Word nameInt = ReadLE2(data + iter); iter += 2;
+         Word type    = ReadLE2(data + iter); iter += 2;
+         scr.codeIdx  = ReadLE4(data + iter); iter += 4;
+         scr.argC     = ReadLE4(data + iter); iter += 4;
+
+         if(nameInt & 0x8000) nameInt |= 0xFFFF0000;
+         setScriptNameTypeACSE(&scr, nameInt, type);
+      }
+
+      return true;
+   }
+
+   //
+   // Module::chunkerACSE_STRE
+   //
+   bool Module::chunkerACSE_STRE(Byte const *data, std::size_t size, Word chunkName)
+   {
+      if(chunkName != MakeID("STRE")) return false;
+
+      std::size_t iter = 0;
+
+      if(size < 12) throw ReadError();
+
+      /*junk      = ReadLE4(data + iter);*/ iter += 4;
+      stringV.alloc(ReadLE4(data + iter));  iter += 4;
+      /*junk      = ReadLE4(data + iter);*/ iter += 4;
+
+      if(size - iter < stringV.size() * 4) throw ReadError();
+      for(String *&str : stringV)
+      {
+         std::size_t offset = ReadLE4(data + iter); iter += 4;
+
+         // Decrypt string.
+         std::unique_ptr<Byte[]> buf;
+         std::size_t             len;
+         std::tie(buf, len) = DecryptStringACSE(data, size, offset);
+
+         // Scan string.
+         Byte const *bufEnd;
+         std::tie(std::ignore, bufEnd, len) = ScanStringACS0(buf.get(), len, 0);
+
+         // Parse string.
+         str = env->getString(ParseStringACS0(buf.get(), bufEnd, len).get(), len);
+      }
+
+      return true;
+   }
+
+   //
+   // Module::chunkerACSE_STRL
+   //
+   bool Module::chunkerACSE_STRL(Byte const *data, std::size_t size, Word chunkName)
+   {
+      if(chunkName != MakeID("STRL")) return false;
+
+      chunkStrTabACSE(stringV, data, size, true);
+
+      return true;
+   }
+
+   //
+   // Module::chunkerACSE_SVCT
+   //
+   bool Module::chunkerACSE_SVCT(Byte const *data, std::size_t size, Word chunkName)
+   {
+      if(chunkName != MakeID("SVCT")) return false;
+
+      if(size % 4) throw ReadError();
+
+      for(std::size_t iter = 0; iter != size;)
+      {
+         Word nameInt = ReadLE2(data + iter); iter += 2;
+         Word regC    = ReadLE2(data + iter); iter += 2;
+
+         if(nameInt & 0x8000) nameInt |= 0xFFFF0000;
+
+         for(Script &scr : scriptV)
+         {
+            if(scr.name.i == nameInt)
+               scr.locRegC = regC;
+         }
+      }
+
+      return false;
+   }
+
+   //
+   // Module::readBytecodeACSE
+   //
+   void Module::readBytecodeACSE(Byte const *data, std::size_t size,
+      bool compressed, std::size_t offset)
+   {
+      std::size_t iter = offset;
+
+      // Find table start.
+      if(iter > size || size - iter < 4) throw ReadError();
+      iter = ReadLE4(data + iter);
+      if(iter > size) throw ReadError();
+
+      // Read chunks.
+      if(offset == 4)
+      {
+         readChunksACSE(data + iter, size - iter, false);
+      }
+      else
+      {
+         if(iter <= offset)
+            readChunksACSE(data + iter, offset - iter, true);
+         else
+            readChunksACSE(data + iter, size - iter, true);
+      }
+
+      // Read code.
+      readCodeACS0(data, size, compressed);
+
+      loaded = true;
+   }
+
+   //
+   // Module::readChunksACSE
+   //
+   void Module::readChunksACSE(Byte const *data, std::size_t size, bool fakeACS0)
+   {
+      // MEXP - Module Variable/Array Export
+      chunkIterACSE(data, size, &Module::chunkerACSE_MEXP);
+
+      // ARAY - Module Arrays
+      chunkIterACSE(data, size, &Module::chunkerACSE_ARAY);
+
+      // AINI - Module Array Init
+      chunkIterACSE(data, size, &Module::chunkerACSE_AINI);
+
+      // FNAM - Function Names
+      chunkIterACSE(data, size, &Module::chunkerACSE_FNAM);
+
+      // FUNC - Functions
+      chunkIterACSE(data, size, &Module::chunkerACSE_FUNC);
+
+      // FARY - Function Arrays
+      chunkIterACSE(data, size, &Module::chunkerACSE_FARY);
+
+      // JUMP - Dynamic Jump Targets
+      chunkIterACSE(data, size, &Module::chunkerACSE_JUMP);
+
+      // MINI - Module Variable Init
+      chunkIterACSE(data, size, &Module::chunkerACSE_MINI);
+
+      // SNAM - Script Names
+      chunkIterACSE(data, size, &Module::chunkerACSE_SNAM);
+
+      // SPTR - Script Pointers
+      if(fakeACS0)
+         chunkIterACSE(data, size, &Module::chunkerACSE_SPTR8);
+      else
+         chunkIterACSE(data, size, &Module::chunkerACSE_SPTR12);
+
+      // SARY - Script Arrays
+      chunkIterACSE(data, size, &Module::chunkerACSE_SARY);
+
+      // SFLG - Script Flags
+      chunkIterACSE(data, size, &Module::chunkerACSE_SFLG);
+
+      // SVCT - Script Variable Count
+      chunkIterACSE(data, size, &Module::chunkerACSE_SVCT);
+
+      // STRE - Encrypted String Literals
+      if(!chunkIterACSE(data, size, &Module::chunkerACSE_STRE))
+      {
+         // STRL - String Literals
+         chunkIterACSE(data, size, &Module::chunkerACSE_STRL);
+      }
+
+      // LOAD - Library Loading
+      chunkIterACSE(data, size, &Module::chunkerACSE_LOAD);
+
+      // Process function imports.
+      for(auto &func : functionV)
+      {
+         if(func) continue;
+
+         std::size_t idx = &func - functionV.data();
+
+         if(idx >= funcNameV.size()) continue;
+
+         auto &funcName = funcNameV[idx];
+
+         if(!funcName) continue;
+
+         for(auto &import : importV)
+         {
+            for(auto &funcImp : import->functionV)
+            {
+               if(funcImp && funcImp->name == funcName)
+               {
+                  func = funcImp;
+                  goto func_found;
+               }
+            }
+         }
+
+      func_found:;
+      }
+
+      // AIMP - Module Array Import
+      chunkIterACSE(data, size, &Module::chunkerACSE_AIMP);
+
+      // MIMP - Module Variable Import
+      chunkIterACSE(data, size, &Module::chunkerACSE_MIMP);
+
+      // ASTR - Module Array Strings
+      chunkIterACSE(data, size, &Module::chunkerACSE_ASTR);
+
+      // ATAG - Module Array Tagging
+      chunkIterACSE(data, size, &Module::chunkerACSE_ATAG);
+
+      // MSTR - Module Variable Strings
+      chunkIterACSE(data, size, &Module::chunkerACSE_MSTR);
+
+      for(auto &init : arrInitV)
+         init.finish();
+   }
+
+   //
+   // Module::setScriptNameTypeACSE
+   //
+   void Module::setScriptNameTypeACSE(Script *scr, Word nameInt, Word type)
+   {
+      // If high bit is set, script is named.
+      if((scr->name.i = nameInt) & 0x80000000)
+      {
+         // Fetch name.
+         Word nameIdx = ~scr->name.i;
+         if(nameIdx < scrNameV.size())
+            scr->name.s = scrNameV[nameIdx];
+      }
+
+      scr->type = env->getScriptTypeACSE(type);
+   }
+
+   //
+   // Module::DecryptStringACSE
+   //
+   std::pair<
+      std::unique_ptr<Byte[]> /*data*/,
+      std::size_t             /*size*/>
+   Module::DecryptStringACSE(Byte const *data, std::size_t size, std::size_t iter)
+   {
+      Word const key = iter * 157135;
+
+      // Calculate length. Start at 1 for null terminator.
+      std::size_t len = 1;
+      for(std::size_t i = iter, n = 0;; ++i, ++n, ++len)
+      {
+         if(i == size) throw ReadError();
+
+         Byte c = static_cast<Byte>(data[i] ^ (n / 2 + key));
+         if(!c) break;
+      }
+
+      // Decrypt data.
+      std::unique_ptr<Byte[]> buf{new Byte[len]};
+      for(std::size_t i = iter, n = 0;; ++i, ++n)
+      {
+         Byte c = static_cast<Byte>(data[i] ^ (n / 2 + key));
+         if(!(buf[n] = c)) break;
+      }
+
+      return {std::move(buf), len};
+   }
+}
+
+// EOF
+
diff --git a/src/acs/vm/ACSVM/PrintBuf.cpp b/src/acs/vm/ACSVM/PrintBuf.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..928585433cfc8bc92bca357cc8e6c2cd89bd7de5
--- /dev/null
+++ b/src/acs/vm/ACSVM/PrintBuf.cpp
@@ -0,0 +1,148 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2015 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// PrintBuf class.
+//
+//-----------------------------------------------------------------------------
+
+#include "PrintBuf.hpp"
+
+#include "BinaryIO.hpp"
+
+#include <cstdio>
+#include <cstdlib>
+#include <new>
+
+
+//----------------------------------------------------------------------------|
+// Extern Functions                                                           |
+//
+
+namespace ACSVM
+{
+   //
+   // PrintBuf constructor
+   //
+   PrintBuf::PrintBuf() :
+      buffer{nullptr},
+      bufEnd{nullptr},
+      bufBeg{nullptr},
+      bufPtr{nullptr}
+   {
+   }
+
+   //
+   // PrintBuf destructor
+   //
+   PrintBuf::~PrintBuf()
+   {
+      std::free(buffer);
+   }
+
+   //
+   // PrintBuf::drop
+   //
+   void PrintBuf::drop()
+   {
+      if(bufBeg != buffer)
+      {
+         bufPtr = bufBeg - 4;
+         bufBeg = bufPtr - ReadLE4(reinterpret_cast<Byte *>(bufPtr));
+      }
+      else
+         bufPtr = bufBeg;
+   }
+
+   //
+   // PrintBuf::format
+   //
+   void PrintBuf::format(char const *fmt, ...)
+   {
+      va_list arg;
+      va_start(arg, fmt);
+      formatv(fmt, arg);
+      va_end(arg);
+   }
+
+   //
+   // PrintBuf::formatv
+   //
+   void PrintBuf::formatv(char const *fmt, va_list arg)
+   {
+      bufPtr += std::vsprintf(bufPtr, fmt, arg);
+   }
+
+   //
+   // PrintBuf::getLoadBuf
+   //
+   char *PrintBuf::getLoadBuf(std::size_t countFull, std::size_t count)
+   {
+      if(static_cast<std::size_t>(bufEnd - buffer) <= countFull)
+      {
+         char *bufNew;
+         if(!(bufNew = static_cast<char *>(std::realloc(buffer, countFull + 1))))
+            throw std::bad_alloc();
+
+         buffer = bufNew;
+         bufEnd = buffer + countFull + 1;
+      }
+
+      bufPtr = buffer + countFull;
+      bufBeg = bufPtr - count;
+
+      return buffer;
+   }
+
+   //
+   // PrintBuf::push
+   //
+   void PrintBuf::push()
+   {
+      reserve(4);
+      WriteLE4(reinterpret_cast<Byte *>(bufPtr), bufPtr - bufBeg);
+      bufBeg = bufPtr += 4;
+   }
+
+   //
+   // PrintBuf::reserve
+   //
+   void PrintBuf::reserve(std::size_t count)
+   {
+      if(static_cast<std::size_t>(bufEnd - bufPtr) > count)
+         return;
+
+      // Allocate extra to anticipate further reserves. +1 for null.
+      count = count * 2 + 1;
+
+      std::size_t idxEnd = bufEnd - buffer;
+      std::size_t idxBeg = bufBeg - buffer;
+      std::size_t idxPtr = bufPtr - buffer;
+
+      // Check for size overflow.
+      if(SIZE_MAX - idxEnd < count)
+         throw std::bad_alloc();
+
+      // Check that the current segment won't pass the push limit.
+      if(UINT32_MAX - (idxEnd - idxBeg) < count)
+         throw std::bad_alloc();
+
+      idxEnd += count;
+
+      char *bufNew;
+      if(!(bufNew = static_cast<char *>(std::realloc(buffer, idxEnd))))
+         throw std::bad_alloc();
+
+      buffer = bufNew;
+      bufEnd = buffer + idxEnd;
+      bufBeg = buffer + idxBeg;
+      bufPtr = buffer + idxPtr;
+   }
+}
+
+// EOF
+
diff --git a/src/acs/vm/ACSVM/PrintBuf.hpp b/src/acs/vm/ACSVM/PrintBuf.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..8cbf64d4828e6f8613b6093123c070f88a14be34
--- /dev/null
+++ b/src/acs/vm/ACSVM/PrintBuf.hpp
@@ -0,0 +1,74 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2015 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// PrintBuf class.
+//
+//-----------------------------------------------------------------------------
+
+#ifndef ACSVM__PrintBuf_H__
+#define ACSVM__PrintBuf_H__
+
+#include "Types.hpp"
+
+#include <cstdarg>
+
+
+//----------------------------------------------------------------------------|
+// Types                                                                      |
+//
+
+namespace ACSVM
+{
+   //
+   // PrintBuf
+   //
+   class PrintBuf
+   {
+   public:
+      PrintBuf();
+      ~PrintBuf();
+
+      void clear() {bufBeg = bufPtr = buffer;}
+
+      char const *data() const {return *bufPtr = '\0', bufBeg;}
+      char const *dataFull() const {return buffer;}
+
+      void drop();
+
+      // Formats using sprintf. Does not reserve space.
+      void format(char const *fmt, ...);
+      void formatv(char const *fmt, std::va_list arg);
+
+      // Returns a pointer to count chars to write into. The caller must write
+      // to the entire returned buffer. Does not reserve space.
+      char *getBuf(std::size_t count)
+         {char *s = bufPtr; bufPtr += count; return s;}
+
+      // Prepares the buffer to be deserialized.
+      char *getLoadBuf(std::size_t countFull, std::size_t count);
+
+      void push();
+
+      // Writes literal characters. Does not reserve space.
+      void put(char c) {*bufPtr++ = c;}
+      void put(char const *s) {while(*s) *bufPtr++ = *s++;}
+      void put(char const *s, std::size_t n) {while(n--) *bufPtr++ = *s++;}
+
+      // Ensures at least count chars are available for writing into.
+      void reserve(std::size_t count);
+
+      std::size_t size() const {return bufPtr - bufBeg;}
+      std::size_t sizeFull() const {return bufPtr - buffer;}
+
+   private:
+      char *buffer, *bufEnd, *bufBeg, *bufPtr;
+   };
+}
+
+#endif//ACSVM__PrintBuf_H__
+
diff --git a/src/acs/vm/ACSVM/Scope.cpp b/src/acs/vm/ACSVM/Scope.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..7f56a7e6fc57528eac81357b31513e834b7cf456
--- /dev/null
+++ b/src/acs/vm/ACSVM/Scope.cpp
@@ -0,0 +1,1299 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2015-2020 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// Scope classes.
+//
+//-----------------------------------------------------------------------------
+
+#include "Scope.hpp"
+
+#include "Action.hpp"
+#include "BinaryIO.hpp"
+#include "Environment.hpp"
+#include "HashMap.hpp"
+#include "HashMapFixed.hpp"
+#include "Init.hpp"
+#include "Module.hpp"
+#include "Script.hpp"
+#include "Serial.hpp"
+#include "Thread.hpp"
+
+#include <algorithm>
+#include <unordered_set>
+#include <vector>
+
+
+//----------------------------------------------------------------------------|
+// Types                                                                      |
+//
+
+namespace ACSVM
+{
+   //
+   // GlobalScope::PrivData
+   //
+   struct GlobalScope::PrivData
+   {
+      HashMapKeyMem<Word, HubScope, &HubScope::id, &HubScope::hashLink> scopes;
+   };
+
+   //
+   // HubScope::PrivData
+   //
+   struct HubScope::PrivData
+   {
+      HashMapKeyMem<Word, MapScope, &MapScope::id, &MapScope::hashLink> scopes;
+   };
+
+   //
+   // MapScope::PrivData
+   //
+   struct MapScope::PrivData
+   {
+      HashMapFixed<Module *, ModuleScope> scopes;
+
+      HashMapFixed<Word,     Script *> scriptInt;
+      HashMapFixed<String *, Script *> scriptStr;
+
+      HashMapFixed<Script *, Thread *> scriptThread;
+   };
+}
+
+
+//----------------------------------------------------------------------------|
+// Extern Objects                                                             |
+//
+
+namespace ACSVM
+{
+   constexpr std::size_t ModuleScope::ArrC;
+   constexpr std::size_t ModuleScope::RegC;
+}
+
+
+//----------------------------------------------------------------------------|
+// Extern Functions                                                           |
+//
+
+namespace ACSVM
+{
+   //
+   // GlobalScope constructor
+   //
+   GlobalScope::GlobalScope(Environment *env_, Word id_) :
+      env{env_},
+      id {id_},
+
+      arrV{},
+      regV{},
+
+      hashLink{this},
+
+      active{false},
+
+      pd{new PrivData}
+   {
+   }
+
+   //
+   // GlobalScope destructor
+   //
+   GlobalScope::~GlobalScope()
+   {
+      reset();
+      delete pd;
+   }
+
+   //
+   // GlobalScope::countActiveThread
+   //
+   std::size_t GlobalScope::countActiveThread() const
+   {
+      std::size_t n = 0;
+
+      for(auto &scope : pd->scopes)
+      {
+         if(scope.active)
+            n += scope.countActiveThread();
+      }
+
+      return n;
+   }
+
+   //
+   // GlobalScope::exec
+   //
+   void GlobalScope::exec()
+   {
+      // Delegate deferred script actions.
+      for(auto itr = scriptAction.begin(), end = scriptAction.end(); itr != end;)
+      {
+         auto scope = pd->scopes.find(itr->id.global);
+         if(scope && scope->active)
+            itr++->link.relink(&scope->scriptAction);
+         else
+            ++itr;
+      }
+
+      for(auto &scope : pd->scopes)
+      {
+         if(scope.active)
+            scope.exec();
+      }
+   }
+
+   //
+   // GlobalScope::freeHubScope
+   //
+   void GlobalScope::freeHubScope(HubScope *scope)
+   {
+      pd->scopes.unlink(scope);
+      delete scope;
+   }
+
+   //
+   // GlobalScope::getHubScope
+   //
+   HubScope *GlobalScope::getHubScope(Word scopeID)
+   {
+      if(auto *scope = pd->scopes.find(scopeID))
+         return scope;
+
+      auto scope = new HubScope(this, scopeID);
+      pd->scopes.insert(scope);
+      return scope;
+   }
+
+   //
+   // GlobalScope::hasActiveThread
+   //
+   bool GlobalScope::hasActiveThread() const
+   {
+      for(auto &scope : pd->scopes)
+      {
+         if(scope.active && scope.hasActiveThread())
+            return true;
+      }
+
+      return false;
+   }
+
+   //
+   // GlobalScope::loadState
+   //
+   void GlobalScope::loadState(Serial &in)
+   {
+      reset();
+
+      in.readSign(Signature::GlobalScope);
+
+      for(auto &arr : arrV)
+         arr.loadState(in);
+
+      for(auto &reg : regV)
+         reg = ReadVLN<Word>(in);
+
+      env->readScriptActions(in, scriptAction);
+
+      active = in.in->get() != '\0';
+
+      for(auto n = ReadVLN<std::size_t>(in); n--;)
+         getHubScope(ReadVLN<Word>(in))->loadState(in);
+
+      in.readSign(~Signature::GlobalScope);
+   }
+
+   //
+   // GlobalScope::lockStrings
+   //
+   void GlobalScope::lockStrings() const
+   {
+      for(auto &arr : arrV) arr.lockStrings(env);
+      for(auto &reg : regV) ++env->getString(reg)->lock;
+
+      for(auto &action : scriptAction)
+         action.lockStrings(env);
+
+      for(auto &scope : pd->scopes)
+         scope.lockStrings();
+   }
+
+   //
+   // GlobalScope::refStrings
+   //
+   void GlobalScope::refStrings() const
+   {
+      for(auto &arr : arrV) arr.refStrings(env);
+      for(auto &reg : regV) env->getString(reg)->ref = true;
+
+      for(auto &action : scriptAction)
+         action.refStrings(env);
+
+      for(auto &scope : pd->scopes)
+         scope.refStrings();
+   }
+
+   //
+   // GlobalScope::reset
+   //
+   void GlobalScope::reset()
+   {
+      while(scriptAction.next->obj)
+         delete scriptAction.next->obj;
+
+      pd->scopes.free();
+   }
+
+   //
+   // GlobalScope::saveState
+   //
+   void GlobalScope::saveState(Serial &out) const
+   {
+      out.writeSign(Signature::GlobalScope);
+
+      for(auto &arr : arrV)
+         arr.saveState(out);
+
+      for(auto &reg : regV)
+         WriteVLN(out, reg);
+
+      env->writeScriptActions(out, scriptAction);
+
+      out.out->put(active ? '\1' : '\0');
+
+      WriteVLN(out, pd->scopes.size());
+      for(auto &scope : pd->scopes)
+      {
+         WriteVLN(out, scope.id);
+         scope.saveState(out);
+      }
+
+      out.writeSign(~Signature::GlobalScope);
+   }
+
+   //
+   // GlobalScope::unlockStrings
+   //
+   void GlobalScope::unlockStrings() const
+   {
+      for(auto &arr : arrV) arr.unlockStrings(env);
+      for(auto &reg : regV) --env->getString(reg)->lock;
+
+      for(auto &action : scriptAction)
+         action.unlockStrings(env);
+
+      for(auto &scope : pd->scopes)
+         scope.unlockStrings();
+   }
+
+   //
+   // HubScope constructor
+   //
+   HubScope::HubScope(GlobalScope *global_, Word id_) :
+      env   {global_->env},
+      global{global_},
+      id    {id_},
+
+      arrV{},
+      regV{},
+
+      hashLink{this},
+
+      active{false},
+
+      pd{new PrivData}
+   {
+   }
+
+   //
+   // HubScope destructor
+   //
+   HubScope::~HubScope()
+   {
+      reset();
+      delete pd;
+   }
+
+   //
+   // HubScope::countActiveThread
+   //
+   std::size_t HubScope::countActiveThread() const
+   {
+      std::size_t n = 0;
+
+      for(auto &scope : pd->scopes)
+      {
+         if(scope.active)
+            n += scope.countActiveThread();
+      }
+
+      return n;
+   }
+
+   //
+   // HubScope::exec
+   //
+   void HubScope::exec()
+   {
+      // Delegate deferred script actions.
+      for(auto itr = scriptAction.begin(), end = scriptAction.end(); itr != end;)
+      {
+         auto scope = pd->scopes.find(itr->id.global);
+         if(scope && scope->active)
+            itr++->link.relink(&scope->scriptAction);
+         else
+            ++itr;
+      }
+
+      for(auto &scope : pd->scopes)
+      {
+         if(scope.active)
+            scope.exec();
+      }
+   }
+
+   //
+   // HubScope::freeMapScope
+   //
+   void HubScope::freeMapScope(MapScope *scope)
+   {
+      pd->scopes.unlink(scope);
+      delete scope;
+   }
+
+   //
+   // HubScope::getMapScope
+   //
+   MapScope *HubScope::getMapScope(Word scopeID)
+   {
+      if(auto *scope = pd->scopes.find(scopeID))
+         return scope;
+
+      auto scope = new MapScope(this, scopeID);
+      pd->scopes.insert(scope);
+      return scope;
+   }
+
+   //
+   // HubScope::hasActiveThread
+   //
+   bool HubScope::hasActiveThread() const
+   {
+      for(auto &scope : pd->scopes)
+      {
+         if(scope.active && scope.hasActiveThread())
+            return true;
+      }
+
+      return false;
+   }
+
+   //
+   // HubScope::loadState
+   //
+   void HubScope::loadState(Serial &in)
+   {
+      reset();
+
+      in.readSign(Signature::HubScope);
+
+      for(auto &arr : arrV)
+         arr.loadState(in);
+
+      for(auto &reg : regV)
+         reg = ReadVLN<Word>(in);
+
+      env->readScriptActions(in, scriptAction);
+
+      active = in.in->get() != '\0';
+
+      for(auto n = ReadVLN<std::size_t>(in); n--;)
+         getMapScope(ReadVLN<Word>(in))->loadState(in);
+
+      in.readSign(~Signature::HubScope);
+   }
+
+   //
+   // HubScope::lockStrings
+   //
+   void HubScope::lockStrings() const
+   {
+      for(auto &arr : arrV) arr.lockStrings(env);
+      for(auto &reg : regV) ++env->getString(reg)->lock;
+
+      for(auto &action : scriptAction)
+         action.lockStrings(env);
+
+      for(auto &scope : pd->scopes)
+         scope.lockStrings();
+   }
+
+   //
+   // HubScope::refStrings
+   //
+   void HubScope::refStrings() const
+   {
+      for(auto &arr : arrV) arr.refStrings(env);
+      for(auto &reg : regV) env->getString(reg)->ref = true;
+
+      for(auto &action : scriptAction)
+         action.refStrings(env);
+
+      for(auto &scope : pd->scopes)
+         scope.refStrings();
+   }
+
+   //
+   // HubScope::reset
+   //
+   void HubScope::reset()
+   {
+      while(scriptAction.next->obj)
+         delete scriptAction.next->obj;
+
+      pd->scopes.free();
+   }
+
+   //
+   // HubScope::saveState
+   //
+   void HubScope::saveState(Serial &out) const
+   {
+      out.writeSign(Signature::HubScope);
+
+      for(auto &arr : arrV)
+         arr.saveState(out);
+
+      for(auto &reg : regV)
+         WriteVLN(out, reg);
+
+      env->writeScriptActions(out, scriptAction);
+
+      out.out->put(active ? '\1' : '\0');
+
+      WriteVLN(out, pd->scopes.size());
+      for(auto &scope : pd->scopes)
+      {
+         WriteVLN(out, scope.id);
+         scope.saveState(out);
+      }
+
+      out.writeSign(~Signature::HubScope);
+   }
+
+   //
+   // HubScope::unlockStrings
+   //
+   void HubScope::unlockStrings() const
+   {
+      for(auto &arr : arrV) arr.unlockStrings(env);
+      for(auto &reg : regV) --env->getString(reg)->lock;
+
+      for(auto &action : scriptAction)
+         action.unlockStrings(env);
+
+      for(auto &scope : pd->scopes)
+         scope.unlockStrings();
+   }
+
+   //
+   // MapScope constructor
+   //
+   MapScope::MapScope(HubScope *hub_, Word id_) :
+      env{hub_->env},
+      hub{hub_},
+      id {id_},
+
+      hashLink{this},
+
+      module0{nullptr},
+
+      active       {false},
+      clampCallSpec{false},
+
+      pd{new PrivData}
+   {
+   }
+
+   //
+   // MapScope destructor
+   //
+   MapScope::~MapScope()
+   {
+      reset();
+      delete pd;
+   }
+
+   //
+   // MapScope::addModules
+   //
+   void MapScope::addModules(Module *const *moduleV, std::size_t moduleC)
+   {
+      module0 = moduleC ? moduleV[0] : nullptr;
+
+      // Find all associated modules.
+
+      struct
+      {
+         std::unordered_set<Module *> set;
+         std::vector<Module *>        vec;
+
+         void add(Module *module)
+         {
+            if(!set.insert(module).second) return;
+
+            vec.push_back(module);
+            for(auto &import : module->importV)
+               add(import);
+         }
+      } modules;
+
+      for(auto itr = moduleV, end = itr + moduleC; itr != end; ++itr)
+         modules.add(*itr);
+
+      // Count scripts.
+
+      std::size_t scriptThrC = 0;
+      std::size_t scriptIntC = 0;
+      std::size_t scriptStrC = 0;
+
+      for(auto &module : modules.vec)
+      {
+         for(auto &script : module->scriptV)
+         {
+            ++scriptThrC;
+            if(script.name.s)
+               ++scriptStrC;
+            else
+               ++scriptIntC;
+         }
+      }
+
+      // Create lookup tables.
+
+      pd->scopes.alloc(modules.vec.size());
+      pd->scriptInt.alloc(scriptIntC);
+      pd->scriptStr.alloc(scriptStrC);
+      pd->scriptThread.alloc(scriptThrC);
+
+      auto scopeItr     = pd->scopes.begin();
+      auto scriptIntItr = pd->scriptInt.begin();
+      auto scriptStrItr = pd->scriptStr.begin();
+      auto scriptThrItr = pd->scriptThread.begin();
+
+      for(auto &module : modules.vec)
+      {
+         using ElemScope = HashMapFixed<Module *, ModuleScope>::Elem;
+
+         new(scopeItr++) ElemScope{module, {this, module}, nullptr};
+
+         for(auto &script : module->scriptV)
+         {
+            using ElemInt = HashMapFixed<Word,     Script *>::Elem;
+            using ElemStr = HashMapFixed<String *, Script *>::Elem;
+            using ElemThr = HashMapFixed<Script *, Thread *>::Elem;
+
+            new(scriptThrItr++) ElemThr{&script, nullptr, nullptr};
+
+            if(script.name.s)
+               new(scriptStrItr++) ElemStr{script.name.s, &script, nullptr};
+            else
+               new(scriptIntItr++) ElemInt{script.name.i, &script, nullptr};
+         }
+      }
+
+      pd->scopes.build();
+      pd->scriptInt.build();
+      pd->scriptStr.build();
+      pd->scriptThread.build();
+
+      for(auto &scope : pd->scopes)
+         scope.val.import();
+   }
+
+   //
+   // MapScope::countActiveThread
+   //
+   std::size_t MapScope::countActiveThread() const
+   {
+      return threadActive.size();
+   }
+
+   //
+   // MapScope::exec
+   //
+   void MapScope::exec()
+   {
+      // Execute deferred script actions.
+      while(scriptAction.next->obj)
+      {
+         ScriptAction *action = scriptAction.next->obj;
+         Script       *script = findScript(action->name);
+
+         if(script) switch(action->action)
+         {
+         case ScriptAction::Start:
+            scriptStart(script, {action->argV.data(), action->argV.size()});
+            break;
+
+         case ScriptAction::StartForced:
+            scriptStartForced(script, {action->argV.data(), action->argV.size()});
+            break;
+
+         case ScriptAction::Stop:
+            scriptStop(script);
+            break;
+
+         case ScriptAction::Pause:
+            scriptPause(script);
+            break;
+         }
+
+         delete action;
+      }
+
+      // Execute running threads.
+      for(auto itr = threadActive.begin(), end = threadActive.end(); itr != end;)
+      {
+         itr->exec();
+         if(itr->state == ThreadState::Inactive)
+            freeThread(&*itr++);
+         else
+            ++itr;
+      }
+   }
+
+   //
+   // MapScope::findScript
+   //
+   Script *MapScope::findScript(ScriptName name)
+   {
+      return name.s ? findScript(name.s) : findScript(name.i);
+   }
+
+   //
+   // MapScope::findScript
+   //
+   Script *MapScope::findScript(String *name)
+   {
+      if(Script **script = pd->scriptStr.find(name))
+         return *script;
+      else
+         return nullptr;
+   }
+
+   //
+   // MapScope::findScript
+   //
+   Script *MapScope::findScript(Word name)
+   {
+      if(Script **script = pd->scriptInt.find(name))
+         return *script;
+      else
+         return nullptr;
+   }
+
+   //
+   // MapScope::freeThread
+   //
+   void MapScope::freeThread(Thread *thread)
+   {
+      auto itr = pd->scriptThread.find(thread->script);
+      if(itr  && *itr == thread)
+         *itr = nullptr;
+
+      env->freeThread(thread);
+   }
+
+   //
+   // MapScope::getModuleScope
+   //
+   ModuleScope *MapScope::getModuleScope(Module *module)
+   {
+      return pd->scopes.find(module);
+   }
+
+   //
+   // MapScope::getString
+   //
+   String *MapScope::getString(Word idx) const
+   {
+      if(idx & 0x80000000)
+         return &env->stringTable[~idx];
+
+      if(!module0 || idx >= module0->stringV.size())
+         return &env->stringTable.getNone();
+
+      return module0->stringV[idx];
+   }
+
+   //
+   // MapScope::hasActiveThread
+   //
+   bool MapScope::hasActiveThread() const
+   {
+      for(auto &thread : threadActive)
+      {
+         if(thread.state != ThreadState::Inactive)
+            return true;
+      }
+
+      return false;
+   }
+
+   //
+   // MapScope::hasModules
+   //
+   bool MapScope::hasModules() const
+   {
+      return !pd->scopes.empty();
+   }
+
+   //
+   // MapScope::isScriptActive
+   //
+   bool MapScope::isScriptActive(Script *script)
+   {
+      auto itr = pd->scriptThread.find(script);
+      return itr && *itr && (*itr)->state != ThreadState::Inactive;
+   }
+
+   //
+   // MapScope::loadModules
+   //
+   void MapScope::loadModules(Serial &in)
+   {
+      auto count = ReadVLN<std::size_t>(in);
+      std::vector<Module *> modules;
+      modules.reserve(count);
+
+      for(auto n = count; n--;)
+         modules.emplace_back(env->getModule(env->readModuleName(in)));
+
+      addModules(modules.data(), modules.size());
+
+      for(auto &module : modules)
+         pd->scopes.find(module)->loadState(in);
+   }
+
+   //
+   // MapScope::loadState
+   //
+   void MapScope::loadState(Serial &in)
+   {
+      reset();
+
+      in.readSign(Signature::MapScope);
+
+      env->readScriptActions(in, scriptAction);
+      active = in.in->get() != '\0';
+      loadModules(in);
+      loadThreads(in);
+
+      in.readSign(~Signature::MapScope);
+   }
+
+   //
+   // MapScope::loadThreads
+   //
+   void MapScope::loadThreads(Serial &in)
+   {
+      for(auto n = ReadVLN<std::size_t>(in); n--;)
+      {
+         Thread *thread = env->getFreeThread();
+         thread->link.insert(&threadActive);
+         thread->loadState(in);
+
+         if(in.in->get())
+         {
+            auto scrThread = pd->scriptThread.find(thread->script);
+            if(scrThread)
+               *scrThread = thread;
+         }
+      }
+   }
+
+   //
+   // MapScope::lockStrings
+   //
+   void MapScope::lockStrings() const
+   {
+      for(auto &action : scriptAction)
+         action.lockStrings(env);
+
+      for(auto &scope : pd->scopes)
+         scope.val.lockStrings();
+
+      for(auto &thread : threadActive)
+         thread.lockStrings();
+   }
+
+   //
+   // MapScope::refStrings
+   //
+   void MapScope::refStrings() const
+   {
+      for(auto &action : scriptAction)
+         action.refStrings(env);
+
+      for(auto &scope : pd->scopes)
+         scope.val.refStrings();
+
+      for(auto &thread : threadActive)
+         thread.refStrings();
+   }
+
+   //
+   // MapScope::reset
+   //
+   void MapScope::reset()
+   {
+      // Stop any remaining threads and return them to free list.
+      while(threadActive.next->obj)
+      {
+         threadActive.next->obj->stop();
+         env->freeThread(threadActive.next->obj);
+      }
+
+      while(scriptAction.next->obj)
+         delete scriptAction.next->obj;
+
+      active = false;
+
+      pd->scopes.free();
+
+      pd->scriptInt.free();
+      pd->scriptStr.free();
+      pd->scriptThread.free();
+   }
+
+   //
+   // MapScope::saveModules
+   //
+   void MapScope::saveModules(Serial &out) const
+   {
+      WriteVLN(out, pd->scopes.size());
+
+      for(auto &scope : pd->scopes)
+         env->writeModuleName(out, scope.key->name);
+
+      for(auto &scope : pd->scopes)
+         scope.val.saveState(out);
+   }
+
+   //
+   // MapScope::saveState
+   //
+   void MapScope::saveState(Serial &out) const
+   {
+      out.writeSign(Signature::MapScope);
+
+      env->writeScriptActions(out, scriptAction);
+      out.out->put(active ? '\1' : '\0');
+      saveModules(out);
+      saveThreads(out);
+
+      out.writeSign(~Signature::MapScope);
+   }
+
+   //
+   // MapScope::saveThreads
+   //
+   void MapScope::saveThreads(Serial &out) const
+   {
+      WriteVLN(out, threadActive.size());
+      for(auto &thread : threadActive)
+      {
+         thread.saveState(out);
+
+         auto scrThread = pd->scriptThread.find(thread.script);
+         out.out->put(scrThread && *scrThread == &thread ? '\1' : '\0');
+      }
+   }
+
+   //
+   // MapScope::scriptPause
+   //
+   bool MapScope::scriptPause(Script *script)
+   {
+      auto itr = pd->scriptThread.find(script);
+      if(!itr || !*itr)
+         return false;
+
+      switch((*itr)->state.state)
+      {
+      case ThreadState::Inactive:
+      case ThreadState::Paused:
+      case ThreadState::Stopped:
+         return false;
+
+      default:
+         (*itr)->state = ThreadState::Paused;
+         return true;
+      }
+   }
+
+   //
+   // MapScope::scriptPause
+   //
+   bool MapScope::scriptPause(ScriptName name, ScopeID scope)
+   {
+      if(scope != ScopeID{hub->global->id, hub->id, id})
+      {
+         env->deferAction({scope, name, ScriptAction::Pause, {}});
+         return true;
+      }
+
+      if(Script *script = findScript(name))
+         return scriptPause(script);
+      else
+         return false;
+   }
+
+   //
+   // MapScope::scriptStart
+   //
+   bool MapScope::scriptStart(Script *script, ScriptStartInfo const &info)
+   {
+      auto itr = pd->scriptThread.find(script);
+      if(!itr)
+         return false;
+
+      if(Thread *&thread = *itr)
+      {
+         switch(thread->state.state)
+         {
+         case ThreadState::Paused:
+            thread->state = ThreadState::Running;
+            return true;
+
+         default:
+            return false;
+         }
+      }
+      else
+      {
+         thread = env->getFreeThread();
+         thread->start(script, this, info.info, info.argV, info.argC);
+         if(info.func) info.func(thread);
+         if(info.funcc) info.funcc(thread);
+         return true;
+      }
+   }
+
+   //
+   // MapScope::scriptStart
+   //
+   bool MapScope::scriptStart(ScriptName name, ScopeID scope, ScriptStartInfo const &info)
+   {
+      if(scope != ScopeID{hub->global->id, hub->id, id})
+      {
+         env->deferAction({scope, name, ScriptAction::Start, {info.argV, info.argC}});
+         return true;
+      }
+
+      if(Script *script = findScript(name))
+         return scriptStart(script, info);
+      else
+         return false;
+   }
+
+   //
+   // MapScope::scriptStartForced
+   //
+   bool MapScope::scriptStartForced(Script *script, ScriptStartInfo const &info)
+   {
+      Thread *thread = env->getFreeThread();
+
+      thread->start(script, this, info.info, info.argV, info.argC);
+      if(info.func) info.func(thread);
+      if(info.funcc) info.funcc(thread);
+      return true;
+   }
+
+   //
+   // MapScope::scriptStartForced
+   //
+   bool MapScope::scriptStartForced(ScriptName name, ScopeID scope, ScriptStartInfo const &info)
+   {
+      if(scope != ScopeID{hub->global->id, hub->id, id})
+      {
+         env->deferAction({scope, name, ScriptAction::StartForced, {info.argV, info.argC}});
+         return true;
+      }
+
+      if(Script *script = findScript(name))
+         return scriptStartForced(script, info);
+      else
+         return false;
+   }
+
+   //
+   // MapScope::scriptStartResult
+   //
+   Word MapScope::scriptStartResult(Script *script, ScriptStartInfo const &info)
+   {
+      Thread *thread = env->getFreeThread();
+
+      thread->start(script, this, info.info, info.argV, info.argC);
+      if(info.func) info.func(thread);
+      if(info.funcc) info.funcc(thread);
+      thread->exec();
+
+      Word result = thread->result;
+      if(thread->state == ThreadState::Inactive)
+         freeThread(thread);
+      return result;
+   }
+
+   //
+   // MapScope::scriptStartResult
+   //
+   Word MapScope::scriptStartResult(ScriptName name, ScriptStartInfo const &info)
+   {
+      if(Script *script = findScript(name))
+         return scriptStartResult(script, info);
+      else
+         return 0;
+   }
+
+   //
+   // MapScope::scriptStartType
+   //
+   Word MapScope::scriptStartType(Word type, ScriptStartInfo const &info)
+   {
+      Word result = 0;
+
+      for(auto &script : pd->scriptThread)
+      {
+         if(script.key->type == type)
+            result += scriptStart(script.key, info);
+      }
+
+      return result;
+   }
+
+   //
+   // MapScope::scriptStartTypeForced
+   //
+   Word MapScope::scriptStartTypeForced(Word type, ScriptStartInfo const &info)
+   {
+      Word result = 0;
+
+      for(auto &script : pd->scriptThread)
+      {
+         if(script.key->type == type)
+            result += scriptStartForced(script.key, info);
+      }
+
+      return result;
+   }
+
+   //
+   // MapScope::scriptStop
+   //
+   bool MapScope::scriptStop(Script *script)
+   {
+      auto itr = pd->scriptThread.find(script);
+      if(!itr || !*itr)
+         return false;
+
+      switch((*itr)->state.state)
+      {
+      case ThreadState::Inactive:
+      case ThreadState::Stopped:
+         return false;
+
+      default:
+         (*itr)->state = ThreadState::Stopped;
+         (*itr)        = nullptr;
+         return true;
+      }
+   }
+
+   //
+   // MapScope::scriptStop
+   //
+   bool MapScope::scriptStop(ScriptName name, ScopeID scope)
+   {
+      if(scope != ScopeID{hub->global->id, hub->id, id})
+      {
+         env->deferAction({scope, name, ScriptAction::Stop, {}});
+         return true;
+      }
+
+      if(Script *script = findScript(name))
+         return scriptStop(script);
+      else
+         return false;
+   }
+
+   //
+   // MapScope::unlockStrings
+   //
+   void MapScope::unlockStrings() const
+   {
+      for(auto &action : scriptAction)
+         action.unlockStrings(env);
+
+      for(auto &scope : pd->scopes)
+         scope.val.unlockStrings();
+
+      for(auto &thread : threadActive)
+         thread.unlockStrings();
+   }
+
+   //
+   // ModuleScope constructor
+   //
+   ModuleScope::ModuleScope(MapScope *map_, Module *module_) :
+      env   {map_->env},
+      map   {map_},
+      module{module_},
+
+      selfArrV{},
+      selfRegV{}
+   {
+      // Set arrays and registers to refer to this scope's by default.
+      for(std::size_t i = 0; i != ArrC; ++i) arrV[i] = &selfArrV[i];
+      for(std::size_t i = 0; i != RegC; ++i) regV[i] = &selfRegV[i];
+
+      // Apply initialization data from module.
+
+      for(std::size_t i = 0; i != ArrC; ++i)
+      {
+         if(i < module->arrInitV.size())
+            module->arrInitV[i].apply(selfArrV[i], module);
+      }
+
+      for(std::size_t i = 0; i != RegC; ++i)
+      {
+         if(i < module->regInitV.size())
+            selfRegV[i] = module->regInitV[i].getValue(module);
+      }
+   }
+
+   //
+   // ModuleScope destructor
+   //
+   ModuleScope::~ModuleScope()
+   {
+   }
+
+   //
+   // ModuleScope::import
+   //
+   void ModuleScope::import()
+   {
+      for(std::size_t i = 0, e = std::min<std::size_t>(ArrC, module->arrImpV.size()); i != e; ++i)
+      {
+         String *arrName = module->arrImpV[i];
+         if(!arrName) continue;
+
+         for(auto &imp : module->importV)
+         {
+            for(auto &impName : imp->arrNameV)
+            {
+               if(impName == arrName)
+               {
+                  std::size_t impIdx = &impName - imp->arrNameV.data();
+                  if(impIdx >= ArrC) continue;
+                  arrV[i] = &map->getModuleScope(imp)->selfArrV[impIdx];
+                  goto arr_found;
+               }
+            }
+         }
+
+      arr_found:;
+      }
+
+      for(std::size_t i = 0, e = std::min<std::size_t>(RegC, module->regImpV.size()); i != e; ++i)
+      {
+         String *regName = module->regImpV[i];
+         if(!regName) continue;
+
+         for(auto &imp : module->importV)
+         {
+            for(auto &impName : imp->regNameV)
+            {
+               if(impName == regName)
+               {
+                  std::size_t impIdx = &impName - imp->regNameV.data();
+                  if(impIdx >= RegC) continue;
+                  regV[i] = &map->getModuleScope(imp)->selfRegV[impIdx];
+                  goto reg_found;
+               }
+            }
+         }
+
+      reg_found:;
+      }
+   }
+
+   //
+   // ModuleScope::loadState
+   //
+   void ModuleScope::loadState(Serial &in)
+   {
+      in.readSign(Signature::ModuleScope);
+
+      for(auto &arr : selfArrV)
+         arr.loadState(in);
+
+      for(auto &reg : selfRegV)
+         reg = ReadVLN<Word>(in);
+
+      in.readSign(~Signature::ModuleScope);
+   }
+
+   //
+   // ModuleScope::lockStrings
+   //
+   void ModuleScope::lockStrings() const
+   {
+      for(auto &arr : selfArrV) arr.lockStrings(env);
+      for(auto &reg : selfRegV) ++env->getString(reg)->lock;
+   }
+
+   //
+   // ModuleScope::refStrings
+   //
+   void ModuleScope::refStrings() const
+   {
+      for(auto &arr : selfArrV) arr.refStrings(env);
+      for(auto &reg : selfRegV) env->getString(reg)->ref = true;
+   }
+
+   //
+   // ModuleScope::saveState
+   //
+   void ModuleScope::saveState(Serial &out) const
+   {
+      out.writeSign(Signature::ModuleScope);
+
+      for(auto &arr : selfArrV)
+         arr.saveState(out);
+
+      for(auto &reg : selfRegV)
+         WriteVLN(out, reg);
+
+      out.writeSign(~Signature::ModuleScope);
+   }
+
+   //
+   // ModuleScope::unlockStrings
+   //
+   void ModuleScope::unlockStrings() const
+   {
+      for(auto &arr : selfArrV) arr.unlockStrings(env);
+      for(auto &reg : selfRegV) --env->getString(reg)->lock;
+   }
+}
+
+// EOF
+
diff --git a/src/acs/vm/ACSVM/Scope.hpp b/src/acs/vm/ACSVM/Scope.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..761e65b2bb5ba76a4171f82fc17b1a5b07c94215
--- /dev/null
+++ b/src/acs/vm/ACSVM/Scope.hpp
@@ -0,0 +1,285 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2015-2017 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// Scope classes.
+//
+//-----------------------------------------------------------------------------
+
+#ifndef ACSVM__Scope_H__
+#define ACSVM__Scope_H__
+
+#include "Array.hpp"
+#include "List.hpp"
+
+
+//----------------------------------------------------------------------------|
+// Types                                                                      |
+//
+
+namespace ACSVM
+{
+   extern "C" using MapScope_ScriptStartFuncC = void (*)(void *);
+
+   //
+   // GlobalScope
+   //
+   class GlobalScope
+   {
+   public:
+      static constexpr std::size_t ArrC = 256;
+      static constexpr std::size_t RegC = 256;
+
+
+      GlobalScope(GlobalScope const &) = delete;
+      GlobalScope(Environment *env, Word id);
+      ~GlobalScope();
+
+      std::size_t countActiveThread() const;
+
+      void exec();
+
+      void freeHubScope(HubScope *scope);
+
+      HubScope *getHubScope(Word id);
+
+      bool hasActiveThread() const;
+
+      void lockStrings() const;
+
+      void loadState(Serial &in);
+
+      void refStrings() const;
+
+      void reset();
+
+      void saveState(Serial &out) const;
+
+      void unlockStrings() const;
+
+      Environment *const env;
+      Word         const id;
+
+      Array arrV[ArrC];
+      Word  regV[RegC];
+
+      ListLink<GlobalScope>  hashLink;
+      ListLink<ScriptAction> scriptAction;
+
+      bool active;
+
+   private:
+      struct PrivData;
+
+      PrivData *pd;
+   };
+
+   //
+   // HubScope
+   //
+   class HubScope
+   {
+   public:
+      static constexpr std::size_t ArrC = 256;
+      static constexpr std::size_t RegC = 256;
+
+
+      HubScope(HubScope const &) = delete;
+      HubScope(GlobalScope *global, Word id);
+      ~HubScope();
+
+      std::size_t countActiveThread() const;
+
+      void exec();
+
+      void freeMapScope(MapScope *scope);
+
+      MapScope *getMapScope(Word id);
+
+      bool hasActiveThread() const;
+
+      void lockStrings() const;
+
+      void loadState(Serial &in);
+
+      void refStrings() const;
+
+      void reset();
+
+      void saveState(Serial &out) const;
+
+      void unlockStrings() const;
+
+      Environment *const env;
+      GlobalScope *const global;
+      Word         const id;
+
+      Array arrV[ArrC];
+      Word  regV[RegC];
+
+      ListLink<HubScope>     hashLink;
+      ListLink<ScriptAction> scriptAction;
+
+      bool active;
+
+   private:
+      struct PrivData;
+
+      PrivData *pd;
+   };
+
+   //
+   // MapScope
+   //
+   class MapScope
+   {
+   public:
+      using ScriptStartFunc = void (*)(Thread *);
+      using ScriptStartFuncC = MapScope_ScriptStartFuncC;
+
+      //
+      // ScriptStartInfo
+      //
+      class ScriptStartInfo
+      {
+      public:
+         ScriptStartInfo() :
+            argV{nullptr}, func{nullptr}, funcc{nullptr}, info{nullptr}, argC{0} {}
+         ScriptStartInfo(Word const *argV_, std::size_t argC_,
+            ThreadInfo const *info_ = nullptr, ScriptStartFunc func_ = nullptr) :
+            argV{argV_}, func{func_}, funcc{nullptr}, info{info_}, argC{argC_} {}
+         ScriptStartInfo(Word const *argV_, std::size_t argC_,
+            ThreadInfo const *info_, ScriptStartFuncC func_) :
+            argV{argV_}, func{nullptr}, funcc{func_}, info{info_}, argC{argC_} {}
+
+         Word       const *argV;
+         ScriptStartFunc   func;
+         ScriptStartFuncC  funcc;
+         ThreadInfo const *info;
+         std::size_t       argC;
+      };
+
+
+      MapScope(MapScope const &) = delete;
+      MapScope(HubScope *hub, Word id);
+      ~MapScope();
+
+      void addModules(Module *const *moduleV, std::size_t moduleC);
+
+      std::size_t countActiveThread() const;
+
+      void exec();
+
+      Script *findScript(ScriptName name);
+      Script *findScript(String *name);
+      Script *findScript(Word name);
+
+      ModuleScope *getModuleScope(Module *module);
+
+      String *getString(Word idx) const;
+
+      bool hasActiveThread() const;
+
+      bool hasModules() const;
+
+      bool isScriptActive(Script *script);
+
+      void loadState(Serial &in);
+
+      void lockStrings() const;
+
+      void refStrings() const;
+
+      void reset();
+
+      void saveState(Serial &out) const;
+
+      bool scriptPause(Script *script);
+      bool scriptPause(ScriptName name, ScopeID scope);
+      bool scriptStart(Script *script, ScriptStartInfo const &info);
+      bool scriptStart(ScriptName name, ScopeID scope, ScriptStartInfo const &info);
+      bool scriptStartForced(Script *script, ScriptStartInfo const &info);
+      bool scriptStartForced(ScriptName name, ScopeID scope, ScriptStartInfo const &info);
+      Word scriptStartResult(Script *script, ScriptStartInfo const &info);
+      Word scriptStartResult(ScriptName name, ScriptStartInfo const &info);
+      Word scriptStartType(Word type, ScriptStartInfo const &info);
+      Word scriptStartTypeForced(Word type, ScriptStartInfo const &info);
+      bool scriptStop(Script *script);
+      bool scriptStop(ScriptName name, ScopeID scope);
+
+      void unlockStrings() const;
+
+      Environment *const env;
+      HubScope    *const hub;
+      Word         const id;
+
+      ListLink<MapScope>     hashLink;
+      ListLink<ScriptAction> scriptAction;
+      ListLink<Thread>       threadActive;
+
+      // Used for untagged string lookup.
+      Module *module0;
+
+      bool active;
+      bool clampCallSpec;
+
+   protected:
+      void freeThread(Thread *thread);
+
+   private:
+      struct PrivData;
+
+      void loadModules(Serial &in);
+      void loadThreads(Serial &in);
+
+      void saveModules(Serial &out) const;
+      void saveThreads(Serial &out) const;
+
+      PrivData *pd;
+   };
+
+   //
+   // ModuleScope
+   //
+   class ModuleScope
+   {
+   public:
+      static constexpr std::size_t ArrC = 256;
+      static constexpr std::size_t RegC = 256;
+
+
+      ModuleScope(ModuleScope const &) = delete;
+      ModuleScope(MapScope *map, Module *module);
+      ~ModuleScope();
+
+      void import();
+
+      void loadState(Serial &in);
+
+      void lockStrings() const;
+
+      void refStrings() const;
+
+      void saveState(Serial &out) const;
+
+      void unlockStrings() const;
+
+      Environment *const env;
+      MapScope    *const map;
+      Module      *const module;
+
+      Array *arrV[ArrC];
+      Word  *regV[RegC];
+
+   private:
+      Array selfArrV[ArrC];
+      Word  selfRegV[RegC];
+   };
+}
+
+#endif//ACSVM__Scope_H__
+
diff --git a/src/acs/vm/ACSVM/Script.cpp b/src/acs/vm/ACSVM/Script.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..15d2d94a732457b417d5a24b6361f80dcc18d3a9
--- /dev/null
+++ b/src/acs/vm/ACSVM/Script.cpp
@@ -0,0 +1,54 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2015-2017 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// Script class.
+//
+//-----------------------------------------------------------------------------
+
+#include "Script.hpp"
+
+#include "Environment.hpp"
+#include "Module.hpp"
+
+
+//----------------------------------------------------------------------------|
+// Extern Fumnctions                                                          |
+//
+
+namespace ACSVM
+{
+   //
+   // Script constructor
+   //
+   Script::Script(Module *module_) :
+      module{module_},
+
+      name{},
+
+      argC   {0},
+      codeIdx{0},
+      flags  {0},
+      locArrC{0},
+      locRegC{module->env->scriptLocRegC},
+      type   {0},
+
+      flagClient{false},
+      flagNet   {false}
+   {
+   }
+
+   //
+   // Script destructor
+   //
+   Script::~Script()
+   {
+   }
+}
+
+// EOF
+
diff --git a/src/acs/vm/ACSVM/Script.hpp b/src/acs/vm/ACSVM/Script.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..33f8f490569ffc841e50af6ae49e5e36fd3fa3f3
--- /dev/null
+++ b/src/acs/vm/ACSVM/Script.hpp
@@ -0,0 +1,66 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2015-2017 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// Script class.
+//
+//-----------------------------------------------------------------------------
+
+#ifndef ACSVM__Script_H__
+#define ACSVM__Script_H__
+
+#include "Types.hpp"
+
+
+//----------------------------------------------------------------------------|
+// Types                                                                      |
+//
+
+namespace ACSVM
+{
+   //
+   // ScriptName
+   //
+   class ScriptName
+   {
+   public:
+      ScriptName() : s{nullptr}, i{0} {}
+      ScriptName(String *s_) : s{s_}, i{0} {}
+      ScriptName(String *s_, Word i_) : s{s_}, i{i_} {}
+      ScriptName(Word i_) : s{nullptr}, i{i_} {}
+
+      String *s;
+      Word    i;
+   };
+
+   //
+   // Script
+   //
+   class Script
+   {
+   public:
+      explicit Script(Module *module);
+      ~Script();
+
+      Module *const module;
+
+      ScriptName name;
+
+      Word argC;
+      Word codeIdx;
+      Word flags;
+      Word locArrC;
+      Word locRegC;
+      Word type;
+
+      bool flagClient : 1;
+      bool flagNet    : 1;
+   };
+}
+
+#endif//ACSVM__Script_H__
+
diff --git a/src/acs/vm/ACSVM/Serial.cpp b/src/acs/vm/ACSVM/Serial.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..83edc0528be18b6f49003032a59ad0304e58533d
--- /dev/null
+++ b/src/acs/vm/ACSVM/Serial.cpp
@@ -0,0 +1,98 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2017 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// Serialization.
+//
+//-----------------------------------------------------------------------------
+
+#include "Serial.hpp"
+
+#include "BinaryIO.hpp"
+#include "Error.hpp"
+
+#include <cstring>
+
+
+//----------------------------------------------------------------------------|
+// Extern Functions                                                           |
+//
+
+namespace ACSVM
+{
+   //
+   // Serial::loadHead
+   //
+   void Serial::loadHead()
+   {
+      char buf[6] = {};
+      in->read(buf, 6);
+
+      if(std::memcmp(buf, "ACSVM\0", 6))
+         throw SerialError{"invalid file signature"};
+
+      version = ReadVLN<unsigned int>(*in);
+
+      auto flags = ReadVLN<std::uint_fast32_t>(*in);
+      signs = flags & 0x0001;
+   }
+
+   //
+   // Serial::loadTail
+   //
+   void Serial::loadTail()
+   {
+      readSign(~Signature::Serial);
+   }
+
+   //
+   // Serial::readSign
+   //
+   void Serial::readSign(Signature sign)
+   {
+      if(!signs) return;
+
+      auto got = static_cast<Signature>(ReadLE4(*in));
+
+      if(sign != got)
+         throw SerialSignError{sign, got};
+   }
+
+   //
+   // Serial::saveHead
+   //
+   void Serial::saveHead()
+   {
+      out->write("ACSVM\0", 6);
+      WriteVLN(*out, 0);
+
+      std::uint_fast32_t flags = 0;
+      if(signs) flags |= 0x0001;
+      WriteVLN(*out, flags);
+   }
+
+   //
+   // Serial::saveTail
+   //
+   void Serial::saveTail()
+   {
+      writeSign(~Signature::Serial);
+   }
+
+   //
+   // Serial::writeSign
+   //
+   void Serial::writeSign(Signature sign)
+   {
+      if(!signs) return;
+
+      WriteLE4(*out, static_cast<std::uint32_t>(sign));
+   }
+}
+
+// EOF
+
diff --git a/src/acs/vm/ACSVM/Serial.hpp b/src/acs/vm/ACSVM/Serial.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..98e1673724d71d443615510e131f1ffc8e1a0bb5
--- /dev/null
+++ b/src/acs/vm/ACSVM/Serial.hpp
@@ -0,0 +1,89 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2017 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// Serialization.
+//
+//-----------------------------------------------------------------------------
+
+#ifndef ACSVM__Serial_H__
+#define ACSVM__Serial_H__
+
+#include "ID.hpp"
+
+#include <istream>
+#include <ostream>
+
+
+//----------------------------------------------------------------------------|
+// Types                                                                      |
+//
+
+namespace ACSVM
+{
+   //
+   // Signature
+   //
+   enum class Signature : std::uint32_t
+   {
+      Array       = MakeID("ARAY"),
+      Environment = MakeID("ENVI"),
+      GlobalScope = MakeID("GBLs"),
+      HubScope    = MakeID("HUBs"),
+      MapScope    = MakeID("MAPs"),
+      ModuleScope = MakeID("MODs"),
+      Serial      = MakeID("SERI"),
+      Thread      = MakeID("THRD"),
+   };
+
+   //
+   // Serial
+   //
+   class Serial
+   {
+   public:
+      Serial(std::istream &in_) : in{&in_} {}
+      Serial(std::ostream &out_) : out{&out_},
+         version{0}, signs{false} {}
+
+      operator std::istream & () {return *in;}
+      operator std::ostream & () {return *out;}
+
+      void loadHead();
+      void loadTail();
+
+      void readSign(Signature sign);
+
+      void saveHead();
+      void saveTail();
+
+      void writeSign(Signature sign);
+
+      union
+      {
+         std::istream *const in;
+         std::ostream *const out;
+      };
+
+      unsigned int version;
+      bool         signs;
+   };
+}
+
+
+//----------------------------------------------------------------------------|
+// Extern Functions                                                           |
+//
+
+namespace ACSVM
+{
+   constexpr Signature operator ~ (Signature sign)
+      {return static_cast<Signature>(~static_cast<std::uint32_t>(sign));}
+}
+
+#endif//ACSVM__Serial_H__
+
diff --git a/src/acs/vm/ACSVM/Stack.hpp b/src/acs/vm/ACSVM/Stack.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..9fb72cc53e4e033357dab1e9396e5006bef93460
--- /dev/null
+++ b/src/acs/vm/ACSVM/Stack.hpp
@@ -0,0 +1,110 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2015 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// Stack class.
+//
+//-----------------------------------------------------------------------------
+
+#ifndef ACSVM__Stack_H__
+#define ACSVM__Stack_H__
+
+#include <climits>
+#include <new>
+#include <utility>
+
+
+//----------------------------------------------------------------------------|
+// Types                                                                      |
+//
+
+namespace ACSVM
+{
+   //
+   // Stack
+   //
+   // Stack container.
+   //
+   template<typename T>
+   class Stack
+   {
+   public:
+      Stack() : stack{nullptr}, stkEnd{nullptr}, stkPtr{nullptr} {}
+      ~Stack() {clear(); ::operator delete(stack);}
+
+      // operator []
+      T &operator [] (std::size_t idx) {return *(stkPtr - idx);}
+
+      // begin
+      T       *begin()       {return stack;}
+      T const *begin() const {return stack;}
+
+      // clear
+      void clear() {while(stkPtr != stack) (--stkPtr)->~T();}
+
+      // drop
+      void drop() {(--stkPtr)->~T();}
+      void drop(std::size_t n) {while(n--) (--stkPtr)->~T();}
+
+      // empty
+      bool empty() const {return stkPtr == stack;}
+
+      // end
+      T       *end()       {return stkPtr;}
+      T const *end() const {return stkPtr;}
+
+      // push
+      void push(T const &value) {new(stkPtr++) T(          value );}
+      void push(T      &&value) {new(stkPtr++) T(std::move(value));}
+
+      //
+      // reserve
+      //
+      void reserve(std::size_t count)
+      {
+         if(static_cast<std::size_t>(stkEnd - stkPtr) >= count)
+            return;
+
+         // Save pointers as indexes.
+         std::size_t idxEnd = stkEnd - stack;
+         std::size_t idxPtr = stkPtr - stack;
+
+         // Calculate new array size.
+         if(SIZE_MAX / sizeof(T) - idxEnd < count * 2)
+            throw std::bad_alloc();
+
+         idxEnd += count * 2;
+
+         // Allocate and initialize new array.
+         T *stackNew = static_cast<T *>(::operator new(idxEnd * sizeof(T)));
+         for(T *itrNew = stackNew, *itr = stack, *end = stkPtr; itr != end;)
+         {
+            new(itrNew++) T(std::move(*itr++));
+            itr->~T();
+         }
+
+         // Free old array.
+         ::operator delete(stack);
+
+         // Restore pointers.
+         stack  = stackNew;
+         stkPtr = stack + idxPtr;
+         stkEnd = stack + idxEnd;
+      }
+
+      // size
+      std::size_t size() const {return stkPtr - stack;}
+
+   private:
+      T *stack;
+      T *stkEnd;
+      T *stkPtr;
+   };
+}
+
+#endif//ACSVM__Stack_H__
+
diff --git a/src/acs/vm/ACSVM/Store.hpp b/src/acs/vm/ACSVM/Store.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..713b4a4b8d573a6d8e6587a78fef0a60083f8568
--- /dev/null
+++ b/src/acs/vm/ACSVM/Store.hpp
@@ -0,0 +1,150 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2015 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// Store class.
+//
+//-----------------------------------------------------------------------------
+
+#ifndef ACSVM__Store_H__
+#define ACSVM__Store_H__
+
+#include "Types.hpp"
+
+#include <new>
+#include <utility>
+
+
+//----------------------------------------------------------------------------|
+// Types                                                                      |
+//
+
+namespace ACSVM
+{
+   //
+   // Store
+   //
+   // Manages storage area for locals.
+   //
+   template<typename T>
+   class Store
+   {
+   public:
+      Store() : store{nullptr}, storeEnd{nullptr}, active{nullptr}, activeEnd{nullptr} {}
+      ~Store() {clear(); ::operator delete(store);}
+
+      // operator []
+      T &operator [] (std::size_t idx) {return active[idx];}
+
+      //
+      // alloc
+      //
+      void alloc(std::size_t count)
+      {
+         // Possibly reallocate underlying storage.
+         if(static_cast<std::size_t>(storeEnd - activeEnd) < count)
+         {
+            // Save pointers as indexes.
+            std::size_t activeIdx    = active    - store;
+            std::size_t activeEndIdx = activeEnd - store;
+            std::size_t storeEndIdx  = storeEnd  - store;
+
+            // Calculate new array size.
+            if(SIZE_MAX / sizeof(T) - storeEndIdx < count * 2)
+               throw std::bad_alloc();
+
+            storeEndIdx += count * 2;
+
+            // Allocate and initialize new array.
+            T *storeNew = static_cast<T *>(::operator new(storeEndIdx * sizeof(T)));
+            for(T *out = storeNew, *in = store, *end = activeEnd; in != end; ++out, ++in)
+            {
+               new(out) T(std::move(*in));
+               in->~T();
+            }
+
+            // Free old array.
+            ::operator delete(store);
+
+            // Restore pointers.
+            store     = storeNew;
+            active    = store + activeIdx;
+            activeEnd = store + activeEndIdx;
+            storeEnd  = store + storeEndIdx;
+         }
+
+         active = activeEnd;
+         while(count--) new(activeEnd++) T{};
+      }
+
+      //
+      // allocLoad
+      //
+      // Allocates storage for loading from saved state. countFull elements are
+      // value-initialized and count elements are made available. That is, they
+      // should correspond to a prior call to sizeFull and size, respectively.
+      //
+      void allocLoad(std::size_t countFull, std::size_t count)
+      {
+         clear();
+         alloc(countFull);
+         active = activeEnd - count;
+      }
+
+      // begin
+      T       *begin()       {return active;}
+      T const *begin() const {return active;}
+
+      // beginFull
+      T       *beginFull()       {return store;}
+      T const *beginFull() const {return store;}
+
+      //
+      // clear
+      //
+      void clear()
+      {
+         while(activeEnd != store)
+           (--activeEnd)->~T();
+
+         active = store;
+      }
+
+      // dataFull
+      T const *dataFull() const {return store;}
+
+      // end
+      T       *end()       {return activeEnd;}
+      T const *end() const {return activeEnd;}
+
+      //
+      // free
+      //
+      // count must be the size (in elements) of the previous allocation.
+      //
+      void free(std::size_t count)
+      {
+        while(activeEnd != active)
+           (--activeEnd)->~T();
+
+        active -= count;
+      }
+
+      // size
+      std::size_t size() const {return activeEnd - active;}
+
+      // sizeFull
+      std::size_t sizeFull() const {return activeEnd - store;}
+
+   private:
+      T *store,  *storeEnd;
+      T *active, *activeEnd;
+   };
+}
+
+#endif//ACSVM__Store_H__
+
diff --git a/src/acs/vm/ACSVM/String.cpp b/src/acs/vm/ACSVM/String.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..d0b0b59524d82ce6cef0a09556bff67f90e0e8c1
--- /dev/null
+++ b/src/acs/vm/ACSVM/String.cpp
@@ -0,0 +1,354 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2015 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// String classes.
+//
+//-----------------------------------------------------------------------------
+
+#include "String.hpp"
+
+#include "BinaryIO.hpp"
+#include "HashMap.hpp"
+
+#include <new>
+#include <vector>
+
+
+//----------------------------------------------------------------------------|
+// Types                                                                      |
+//
+
+namespace ACSVM
+{
+   //
+   // StringTable::PrivData
+   //
+   struct StringTable::PrivData
+   {
+      std::vector<Word> freeIdx;
+
+      HashMapKeyObj<StringData, String, &String::link> stringByData{64, 64};
+      std::vector<String *>                            stringByIdx;
+   };
+}
+
+
+//----------------------------------------------------------------------------|
+// Extern Functions                                                           |
+//
+
+namespace ACSVM
+{
+   //
+   // String constructor
+   //
+   String::String(StringData const &data, Word idx_) :
+      StringData{data}, lock{0}, idx{idx_}, len0(std::strlen(str)), link{this}
+   {
+   }
+
+   //
+   // String destructor
+   //
+   String::~String()
+   {
+   }
+
+   //
+   // String::Delete
+   //
+   void String::Delete(String *str)
+   {
+      str->~String();
+      operator delete(str);
+   }
+
+   //
+   // String::New
+   //
+   String *String::New(StringData const &data, Word idx)
+   {
+      String *str = static_cast<String *>(operator new(sizeof(String) + data.len + 1));
+      char   *buf = reinterpret_cast<char *>(str + 1);
+
+      memcpy(buf, data.str, data.len);
+      buf[data.len] = '\0';
+
+      return new(str) String{{buf, data.len, data.hash}, idx};
+   }
+
+   //
+   // String::Read
+   //
+   String *String::Read(std::istream &in, Word idx)
+   {
+      std::size_t len = ReadVLN<std::size_t>(in);
+
+      String *str = static_cast<String *>(operator new(sizeof(String) + len + 1));
+      char   *buf = reinterpret_cast<char *>(str + 1);
+
+      in.read(buf, len);
+      buf[len] = '\0';
+
+      return new(str) String{{buf, len, StrHash(buf, len)}, idx};
+   }
+
+   //
+   // String::Write
+   //
+   void String::Write(std::ostream &out, String *in)
+   {
+      WriteVLN(out, in->len);
+      out.write(in->str, in->len);
+   }
+
+   //
+   // StringTable constructor
+   //
+   StringTable::StringTable() :
+      strV{nullptr},
+      strC{0},
+
+      strNone{String::New({"", 0, 0}, 0)},
+
+      pd{new PrivData}
+   {
+   }
+
+   //
+   // StringTable move constructor
+   //
+   StringTable::StringTable(StringTable &&table) :
+      strV{table.strV},
+      strC{table.strC},
+
+      strNone{table.strNone},
+
+      pd{table.pd}
+   {
+      table.strV = nullptr;
+      table.strC = 0;
+
+      table.strNone = nullptr;
+
+      table.pd = nullptr;
+   }
+
+   //
+   // StringTable destructor
+   //
+   StringTable::~StringTable()
+   {
+      if(!pd) return;
+
+      clear();
+
+      delete pd;
+
+      String::Delete(strNone);
+   }
+
+   //
+   // StringTable::operator [StringData]
+   //
+   String &StringTable::operator [] (StringData const &data)
+   {
+      if(auto str = pd->stringByData.find(data)) return *str;
+
+      Word idx;
+      if(pd->freeIdx.empty())
+      {
+         // Index has to fit within Word size.
+         // If size_t has an equal or lesser max, then the check is redundant,
+         // and some compilers warn about that kind of tautological comparison.
+         #if SIZE_MAX > UINT32_MAX
+         if(pd->stringByIdx.size() > UINT32_MAX)
+            throw std::bad_alloc();
+         #endif
+
+         idx = pd->stringByIdx.size();
+         pd->stringByIdx.emplace_back(strNone);
+         strV = pd->stringByIdx.data();
+         strC = pd->stringByIdx.size();
+      }
+      else
+      {
+         idx = pd->freeIdx.back();
+         pd->freeIdx.pop_back();
+      }
+
+      String *str = String::New(data, idx);
+      pd->stringByIdx[idx] = str;
+      pd->stringByData.insert(str);
+      return *str;
+   }
+
+   //
+   // StringTable::clear
+   //
+   void StringTable::clear()
+   {
+      for(auto &str : pd->stringByIdx)
+      {
+         if(str != strNone)
+            String::Delete(str);
+      }
+
+      pd->freeIdx.clear();
+      pd->stringByData.clear();
+      pd->stringByIdx.clear();
+
+      strV = nullptr;
+      strC = 0;
+   }
+
+   //
+   // StringTable::collectBegin
+   //
+   void StringTable::collectBegin()
+   {
+      for(auto &str : pd->stringByData)
+         str.ref = false;
+   }
+
+   //
+   // StringTable.collectEnd
+   //
+   void StringTable::collectEnd()
+   {
+      for(auto itr = pd->stringByData.begin(), end = pd->stringByData.end(); itr != end;)
+      {
+         if(!itr->ref && !itr->lock)
+         {
+            String &str = *itr++;
+            pd->stringByIdx[str.idx] = strNone;
+            pd->freeIdx.push_back(str.idx);
+            pd->stringByData.unlink(&str);
+            String::Delete(&str);
+         }
+         else
+            ++itr;
+      }
+   }
+
+   //
+   // StringTable::loadState
+   //
+   void StringTable::loadState(std::istream &in)
+   {
+      if(pd)
+      {
+         clear();
+      }
+      else
+      {
+         pd      = new PrivData;
+         strNone = String::New({"", 0, 0}, 0);
+      }
+
+      auto count = ReadVLN<std::size_t>(in);
+
+      pd->stringByIdx.resize(count);
+      strV = pd->stringByIdx.data();
+      strC = pd->stringByIdx.size();
+
+      for(std::size_t idx = 0; idx != count; ++idx)
+      {
+         if(in.get())
+         {
+            String *str = String::Read(in, idx);
+            str->lock = ReadVLN<std::size_t>(in);
+            pd->stringByIdx[idx] = str;
+            pd->stringByData.insert(str);
+         }
+         else
+         {
+            pd->stringByIdx[idx] = strNone;
+            pd->freeIdx.emplace_back(idx);
+         }
+      }
+   }
+
+   //
+   // StringTable::saveState
+   //
+   void StringTable::saveState(std::ostream &out) const
+   {
+      WriteVLN(out, pd->stringByIdx.size());
+
+      for(String *&str : pd->stringByIdx)
+      {
+         if(str != strNone)
+         {
+            out << '\1';
+
+            String::Write(out, str);
+            WriteVLN(out, str->lock);
+         }
+         else
+            out << '\0';
+      }
+   }
+
+   //
+   // StringTable::size
+   //
+   std::size_t StringTable::size() const
+   {
+      return pd->stringByData.size();
+   }
+
+   //
+   // StrDup
+   //
+   std::unique_ptr<char[]> StrDup(char const *str)
+   {
+      return StrDup(str, std::strlen(str));
+   }
+
+   //
+   // StrDup
+   //
+   std::unique_ptr<char[]> StrDup(char const *str, std::size_t len)
+   {
+      std::unique_ptr<char[]> dup{new char[len + 1]};
+      std::memcpy(dup.get(), str, len);
+      dup[len] = '\0';
+
+      return dup;
+   }
+
+   //
+   // StrHash
+   //
+   std::size_t StrHash(char const *str)
+   {
+      std::size_t hash = 0;
+
+      if(str) while(*str)
+         hash = hash * 5 + static_cast<unsigned char>(*str++);
+
+      return hash;
+   }
+
+   //
+   // StrHash
+   //
+   std::size_t StrHash(char const *str, std::size_t len)
+   {
+      std::size_t hash = 0;
+
+      while(len--)
+         hash = hash * 5 + static_cast<unsigned char>(*str++);
+
+      return hash;
+   }
+}
+
+// EOF
+
diff --git a/src/acs/vm/ACSVM/String.hpp b/src/acs/vm/ACSVM/String.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..7c46e4f9205bb4572c71e30cedeff6d8d962e0c2
--- /dev/null
+++ b/src/acs/vm/ACSVM/String.hpp
@@ -0,0 +1,159 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2015 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// String classes.
+//
+//-----------------------------------------------------------------------------
+
+#ifndef ACSVM__String_H__
+#define ACSVM__String_H__
+
+#include "List.hpp"
+#include "Types.hpp"
+
+#include <cstring>
+#include <functional>
+#include <memory>
+
+
+//----------------------------------------------------------------------------|
+// Types                                                                      |
+//
+
+namespace ACSVM
+{
+   std::size_t StrHash(char const *str, std::size_t len);
+
+   //
+   // StringData
+   //
+   // Stores basic string information. Does not manage the storage for the
+   // string data.
+   //
+   class StringData
+   {
+   public:
+      StringData(char const *first, char const *last) :
+         str{first}, len(last - first), hash{StrHash(str, len)} {}
+      StringData(char const *str_, std::size_t len_) :
+         str{str_}, len{len_}, hash{StrHash(str, len)} {}
+      StringData(char const *str_, std::size_t len_, std::size_t hash_) :
+         str{str_}, len{len_}, hash{hash_} {}
+
+      bool operator == (StringData const &r) const
+         {return hash == r.hash && len == r.len && !std::memcmp(str, r.str, len);}
+
+      char const *const str;
+      std::size_t const len;
+      std::size_t const hash;
+   };
+
+   //
+   // String
+   //
+   // Indexed string data.
+   //
+   class String : public StringData
+   {
+   public:
+      std::size_t lock;
+
+      Word const idx;  // Index into table.
+      Word const len0; // Null-terminated length.
+
+      bool ref;
+
+      char get(std::size_t i) const {return i < len ? str[i] : '\0';}
+
+
+      friend class StringTable;
+
+   private:
+      String(StringData const &data, Word idx);
+      ~String();
+
+      ListLink<String> link;
+
+
+      static void Delete(String *str);
+
+      static String *New(StringData const &data, Word idx);
+
+      static String *Read(std::istream &in, Word idx);
+
+      static void Write(std::ostream &out, String *in);
+   };
+
+   //
+   // StringTable
+   //
+   class StringTable
+   {
+   public:
+      StringTable();
+      StringTable(StringTable &&table);
+      ~StringTable();
+
+      String &operator [] (Word idx) const
+         {return idx < strC ? *strV[idx] : *strNone;}
+      String &operator [] (StringData const &data);
+
+      void clear();
+
+      void collectBegin();
+      void collectEnd();
+
+      String &getNone() {return *strNone;}
+
+      void loadState(std::istream &in);
+
+      void saveState(std::ostream &out) const;
+
+      std::size_t size() const;
+
+   private:
+      struct PrivData;
+
+      String    **strV;
+      std::size_t strC;
+
+      String *strNone;
+
+      PrivData *pd;
+   };
+}
+
+namespace std
+{
+   //
+   // hash<::ACSVM::StringData>
+   //
+   template<>
+   struct hash<::ACSVM::StringData>
+   {
+      size_t operator () (::ACSVM::StringData const &data) const
+         {return data.hash;}
+   };
+}
+
+
+//----------------------------------------------------------------------------|
+// Extern Functions                                                           |
+//
+
+namespace ACSVM
+{
+   std::unique_ptr<char[]> StrDup(char const *str);
+   std::unique_ptr<char[]> StrDup(char const *str, std::size_t len);
+
+   std::size_t StrHash(char const *str);
+   std::size_t StrHash(char const *str, std::size_t len);
+}
+
+#endif//ACSVM__String_H__
+
diff --git a/src/acs/vm/ACSVM/Thread.cpp b/src/acs/vm/ACSVM/Thread.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..c4d19a7a8c30a070656f35a3732a6804ba627340
--- /dev/null
+++ b/src/acs/vm/ACSVM/Thread.cpp
@@ -0,0 +1,292 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2015-2017 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// Thread classes.
+//
+//-----------------------------------------------------------------------------
+
+#include "Thread.hpp"
+
+#include "Array.hpp"
+#include "BinaryIO.hpp"
+#include "Environment.hpp"
+#include "Module.hpp"
+#include "Scope.hpp"
+#include "Script.hpp"
+#include "Serial.hpp"
+
+#include <algorithm>
+
+
+//----------------------------------------------------------------------------|
+// Extern Functions                                                           |
+//
+
+namespace ACSVM
+{
+   //
+   // Thread constructor
+   //
+   Thread::Thread(Environment *env_) :
+      env{env_},
+
+      link{this},
+
+      codePtr {nullptr},
+      module  {nullptr},
+      scopeGbl{nullptr},
+      scopeHub{nullptr},
+      scopeMap{nullptr},
+      scopeMod{nullptr},
+      script  {nullptr},
+      delay   {0},
+      result  {0}
+   {
+   }
+
+   //
+   // Thread destructor
+   //
+   Thread::~Thread()
+   {
+   }
+
+   //
+   // Thread::getInfo
+   //
+   ThreadInfo const *Thread::getInfo() const
+   {
+      return nullptr;
+   }
+
+   //
+   // Thread::loadState
+   //
+   void Thread::loadState(Serial &in)
+   {
+      std::size_t count, countFull;
+
+      in.readSign(Signature::Thread);
+
+      module   = env->getModule(env->readModuleName(in));
+      codePtr  = &module->codeV[ReadVLN<std::size_t>(in)];
+      scopeGbl = env->getGlobalScope(ReadVLN<Word>(in));
+      scopeHub = scopeGbl->getHubScope(ReadVLN<Word>(in));
+      scopeMap = scopeHub->getMapScope(ReadVLN<Word>(in));
+      scopeMod = scopeMap->getModuleScope(module);
+      script   = env->readScript(in);
+      delay    = ReadVLN<Word>(in);
+      result   = ReadVLN<Word>(in);
+
+      count = ReadVLN<std::size_t>(in);
+      callStk.clear(); callStk.reserve(count + CallStkSize);
+      while(count--)
+         callStk.push(readCallFrame(in));
+
+      count = ReadVLN<std::size_t>(in);
+      dataStk.clear(); dataStk.reserve(count + DataStkSize);
+      while(count--)
+         dataStk.push(ReadVLN<Word>(in));
+
+      countFull = ReadVLN<std::size_t>(in);
+      count     = ReadVLN<std::size_t>(in);
+      localArr.allocLoad(countFull, count);
+      for(auto itr = localArr.beginFull(), end = localArr.end(); itr != end; ++itr)
+         itr->loadState(in);
+
+      countFull = ReadVLN<std::size_t>(in);
+      count     = ReadVLN<std::size_t>(in);
+      localReg.allocLoad(countFull, count);
+      for(auto itr = localReg.beginFull(), end = localReg.end(); itr != end; ++itr)
+         *itr = ReadVLN<Word>(in);
+
+      countFull = ReadVLN<std::size_t>(in);
+      count     = ReadVLN<std::size_t>(in);
+      in.in->read(printBuf.getLoadBuf(countFull, count), countFull);
+
+      state.state = static_cast<ThreadState::State>(ReadVLN<int>(in));
+      state.data = ReadVLN<Word>(in);
+      state.type = ReadVLN<Word>(in);
+
+      in.readSign(~Signature::Thread);
+   }
+
+   //
+   // Thread::lockStrings
+   //
+   void Thread::lockStrings() const
+   {
+      for(auto &data : dataStk)
+         ++env->getString(data)->lock;
+
+      for(auto arr = localArr.beginFull(), end = localArr.end(); arr != end; ++arr)
+         arr->lockStrings(env);
+
+      for(auto reg = localReg.beginFull(), end = localReg.end(); reg != end; ++reg)
+         ++env->getString(*reg)->lock;
+
+      if(state == ThreadState::WaitScrS)
+         ++env->getString(state.data)->lock;
+   }
+
+   //
+   // Thread::readCallFrame
+   //
+   CallFrame Thread::readCallFrame(Serial &in) const
+   {
+      CallFrame out;
+
+      out.module   = env->getModule(env->readModuleName(in));
+      out.scopeMod = scopeMap->getModuleScope(out.module);
+      out.codePtr  = &out.module->codeV[ReadVLN<std::size_t>(in)];
+      out.locArrC  = ReadVLN<std::size_t>(in);
+      out.locRegC  = ReadVLN<std::size_t>(in);
+
+      return out;
+   }
+
+   //
+   // Thread::refStrings
+   //
+   void Thread::refStrings() const
+   {
+      for(auto &data : dataStk)
+         env->getString(data)->ref = true;
+
+      for(auto arr = localArr.beginFull(), end = localArr.end(); arr != end; ++arr)
+         arr->refStrings(env);
+
+      for(auto reg = localReg.beginFull(), end = localReg.end(); reg != end; ++reg)
+         env->getString(*reg)->ref = true;
+
+      if(state == ThreadState::WaitScrS)
+         env->getString(state.data)->ref = true;
+   }
+
+   //
+   // Thread::saveState
+   //
+   void Thread::saveState(Serial &out) const
+   {
+      out.writeSign(Signature::Thread);
+
+      env->writeModuleName(out, module->name);
+      WriteVLN(out, codePtr - module->codeV.data());
+      WriteVLN(out, scopeGbl->id);
+      WriteVLN(out, scopeHub->id);
+      WriteVLN(out, scopeMap->id);
+      env->writeScript(out, script);
+      WriteVLN(out, delay);
+      WriteVLN(out, result);
+
+      WriteVLN(out, callStk.size());
+      for(auto &call : callStk)
+         writeCallFrame(out, call);
+
+      WriteVLN(out, dataStk.size());
+      for(auto &data : dataStk)
+         WriteVLN(out, data);
+
+      WriteVLN(out, localArr.sizeFull());
+      WriteVLN(out, localArr.size());
+      for(auto itr = localArr.beginFull(), end = localArr.end(); itr != end; ++itr)
+         itr->saveState(out);
+
+      WriteVLN(out, localReg.sizeFull());
+      WriteVLN(out, localReg.size());
+      for(auto itr = localReg.beginFull(), end = localReg.end(); itr != end; ++itr)
+         WriteVLN(out, *itr);
+
+      WriteVLN(out, printBuf.sizeFull());
+      WriteVLN(out, printBuf.size());
+      out.out->write(printBuf.dataFull(), printBuf.sizeFull());
+
+      WriteVLN<int>(out, state.state);
+      WriteVLN(out, state.data);
+      WriteVLN(out, state.type);
+
+      out.writeSign(~Signature::Thread);
+   }
+
+   //
+   // Thread::start
+   //
+   void Thread::start(Script *script_, MapScope *map, ThreadInfo const *,
+      Word const *argV, Word argC)
+   {
+      link.insert(&map->threadActive);
+
+      script  = script_;
+      module  = script->module;
+      codePtr = &module->codeV[script->codeIdx];
+
+      scopeMod = map->getModuleScope(module);
+      scopeMap = map;
+      scopeHub = scopeMap->hub;
+      scopeGbl = scopeHub->global;
+
+      callStk.reserve(CallStkSize);
+      dataStk.reserve(DataStkSize);
+      localArr.alloc(script->locArrC);
+      localReg.alloc(script->locRegC);
+
+      std::copy(argV, argV + std::min<Word>(argC, script->argC), &localReg[0]);
+
+      delay  = 0;
+      result = 0;
+      state  = ThreadState::Running;
+   }
+
+   //
+   // Thread::stop
+   //
+   void Thread::stop()
+   {
+      // Release execution resources.
+      callStk.clear();
+      dataStk.clear();
+      localArr.clear();
+      localReg.clear();
+      printBuf.clear();
+
+      // Set state.
+      state = ThreadState::Inactive;
+   }
+
+   //
+   // Thread::unlockStrings
+   //
+   void Thread::unlockStrings() const
+   {
+      for(auto &data : dataStk)
+         --env->getString(data)->lock;
+
+      for(auto arr = localArr.beginFull(), end = localArr.end(); arr != end; ++arr)
+         arr->unlockStrings(env);
+
+      for(auto reg = localReg.beginFull(), end = localReg.end(); reg != end; ++reg)
+         --env->getString(*reg)->lock;
+
+      if(state == ThreadState::WaitScrS)
+         --env->getString(state.data)->lock;
+   }
+
+   //
+   // Thread::writeCallFrame
+   //
+   void Thread::writeCallFrame(Serial &out, CallFrame const &in) const
+   {
+      env->writeModuleName(out, in.module->name);
+      WriteVLN(out, in.codePtr - in.module->codeV.data());
+      WriteVLN(out, in.locArrC);
+      WriteVLN(out, in.locRegC);
+   }
+}
+
+// EOF
+
diff --git a/src/acs/vm/ACSVM/Thread.hpp b/src/acs/vm/ACSVM/Thread.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..06d4b34d0d11173888ad51eb21f668862ff22c4b
--- /dev/null
+++ b/src/acs/vm/ACSVM/Thread.hpp
@@ -0,0 +1,157 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2015-2017 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// Thread classes.
+//
+//-----------------------------------------------------------------------------
+
+#ifndef ACSVM__Thread_H__
+#define ACSVM__Thread_H__
+
+#include "List.hpp"
+#include "PrintBuf.hpp"
+#include "Stack.hpp"
+#include "Store.hpp"
+
+
+//----------------------------------------------------------------------------|
+// Types                                                                      |
+//
+
+namespace ACSVM
+{
+   //
+   // CallFrame
+   //
+   // Stores a call frame for execution.
+   //
+   class CallFrame
+   {
+   public:
+      Word  const *codePtr;
+      Module      *module;
+      ModuleScope *scopeMod;
+      std::size_t  locArrC;
+      std::size_t  locRegC;
+   };
+
+   //
+   // ThreadState
+   //
+   class ThreadState
+   {
+   public:
+      enum State
+      {
+         Inactive, // Inactive thread.
+         Running,  // Running.
+         Stopped,  // Will go inactive on next exec.
+         Paused,   // Paused by instruction.
+         WaitScrI, // Waiting on a numbered script.
+         WaitScrS, // Waiting on a named script.
+         WaitTag,  // Waiting on tagged object.
+      };
+
+
+      ThreadState() : state{Inactive}, data{0}, type{0} {}
+      ThreadState(State state_) :
+         state{state_}, data{0}, type{0} {}
+      ThreadState(State state_, Word data_) :
+         state{state_}, data{data_}, type{0} {}
+      ThreadState(State state_, Word data_, Word type_) :
+         state{state_}, data{data_}, type{type_} {}
+
+      bool operator == (State s) const {return state == s;}
+      bool operator != (State s) const {return state != s;}
+
+      State state;
+
+      // Extra state data. Used by:
+      //    WaitScrI - Script number.
+      //    WaitScrS - Script name index.
+      //    WaitTag  - Tag number.
+      Word data;
+
+      // Extra state data. Used by:
+      //    WaitTag - Tag type.
+      Word type;
+   };
+
+   //
+   // ThreadInfo
+   //
+   // Derived classes can be used to pass extra information to started threads.
+   //
+   class ThreadInfo
+   {
+   public:
+      virtual ~ThreadInfo() {}
+   };
+
+   //
+   // Thread
+   //
+   class Thread
+   {
+   public:
+      Thread(Environment *env);
+      virtual ~Thread();
+
+      void exec();
+
+      virtual ThreadInfo const *getInfo() const;
+
+      virtual void loadState(Serial &in);
+
+      virtual void lockStrings() const;
+
+      virtual void refStrings() const;
+
+      virtual void saveState(Serial &out) const;
+
+      virtual void start(Script *script, MapScope *map, ThreadInfo const *info,
+         Word const *argV, Word argC);
+
+      virtual void stop();
+
+      virtual void unlockStrings() const;
+
+      Environment *const env;
+
+      ListLink<Thread> link;
+
+      Stack<CallFrame> callStk;
+      Stack<Word>      dataStk;
+      Store<Array>     localArr;
+      Store<Word>      localReg;
+      PrintBuf         printBuf;
+      ThreadState      state;
+
+      Word  const *codePtr; // Instruction pointer.
+      Module      *module;  // Current execution Module.
+      GlobalScope *scopeGbl;
+      HubScope    *scopeHub;
+      MapScope    *scopeMap;
+      ModuleScope *scopeMod;
+      Script      *script;  // Current execution Script.
+      Word         delay;   // Execution delay tics.
+      Word         result;  // Code-defined thread result.
+
+
+      static constexpr std::size_t CallStkSize =   8;
+      static constexpr std::size_t DataStkSize = 256;
+
+   private:
+      CallFrame readCallFrame(Serial &in) const;
+
+      void writeCallFrame(Serial &out, CallFrame const &in) const;
+   };
+}
+
+#endif//ACSVM__Thread_H__
+
diff --git a/src/acs/vm/ACSVM/ThreadExec.cpp b/src/acs/vm/ACSVM/ThreadExec.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..6754800a4e3cd5594088c3a709932cd52db07e45
--- /dev/null
+++ b/src/acs/vm/ACSVM/ThreadExec.cpp
@@ -0,0 +1,643 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2015-2017 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// Thread execution.
+//
+//-----------------------------------------------------------------------------
+
+#include "Thread.hpp"
+
+#include "Array.hpp"
+#include "Code.hpp"
+#include "Environment.hpp"
+#include "Function.hpp"
+#include "Jump.hpp"
+#include "Module.hpp"
+#include "Scope.hpp"
+#include "Script.hpp"
+
+
+//----------------------------------------------------------------------------|
+// Macros                                                                     |
+//
+
+//
+// ACSVM_DynamicGoto
+//
+// If nonzero, enables use of dynamic goto labels in core interpreter loop.
+// Currently, only gcc syntax is supported.
+//
+#ifndef ACSVM_DynamicGoto
+#if defined(__GNUC__)
+#define ACSVM_DynamicGoto 1
+#else
+#define ACSVM_DynamicGoto 0
+#endif
+#endif
+
+//
+// BranchTo
+//
+#define BranchTo(target) \
+   do \
+   { \
+      codePtr = &module->codeV[(target)]; \
+      CountBranch(); \
+   } \
+   while(0)
+
+//
+// CountBranch
+//
+// Used to limit the number of branches to prevent infinite no-delay loops.
+//
+#define CountBranch() \
+   if(branches && !--branches) \
+   { \
+      env->printKill(this, static_cast<Word>(KillType::BranchLimit), 0); \
+      goto thread_stop; \
+   } \
+   else \
+      ((void)0)
+
+//
+// DeclCase
+//
+#if ACSVM_DynamicGoto
+#define DeclCase(name) case_Code##name
+#else
+#define DeclCase(name) case static_cast<Word>(Code::name)
+#endif
+
+//
+// NextCase
+//
+#if ACSVM_DynamicGoto
+#define NextCase() goto *cases[*codePtr++]
+#else
+#define NextCase() goto next_case
+#endif
+
+//
+// Op_*
+//
+
+#define Op_AddU(lop) (dataStk.drop(), (lop) += dataStk[0])
+#define Op_AndU(lop) (dataStk.drop(), (lop) &= dataStk[0])
+#define Op_CmpI_GE(lop) (dataStk.drop(), OpFunc_CmpI_GE(lop, dataStk[0]))
+#define Op_CmpI_GT(lop) (dataStk.drop(), OpFunc_CmpI_GT(lop, dataStk[0]))
+#define Op_CmpI_LE(lop) (dataStk.drop(), OpFunc_CmpI_LE(lop, dataStk[0]))
+#define Op_CmpI_LT(lop) (dataStk.drop(), OpFunc_CmpI_LT(lop, dataStk[0]))
+#define Op_CmpU_EQ(lop) (dataStk.drop(), OpFunc_CmpU_EQ(lop, dataStk[0]))
+#define Op_CmpU_NE(lop) (dataStk.drop(), OpFunc_CmpU_NE(lop, dataStk[0]))
+#define Op_DecU(lop) (--(lop))
+#define Op_DivI(lop) (dataStk.drop(), OpFunc_DivI(lop, dataStk[0]))
+#define Op_DivX(lop) (dataStk.drop(), OpFunc_DivX(lop, dataStk[0]))
+#define Op_Drop(lop) (dataStk.drop(), (lop) = dataStk[0])
+#define Op_IncU(lop) (++(lop))
+#define Op_LAnd(lop) (dataStk.drop(), OpFunc_LAnd(lop, dataStk[0]))
+#define Op_LOrI(lop) (dataStk.drop(), OpFunc_LOrI(lop, dataStk[0]))
+#define Op_ModI(lop) (dataStk.drop(), OpFunc_ModI(lop, dataStk[0]))
+#define Op_MulU(lop) (dataStk.drop(), (lop) *= dataStk[0])
+#define Op_MulX(lop) (dataStk.drop(), OpFunc_MulX(lop, dataStk[0]))
+#define Op_OrIU(lop) (dataStk.drop(), (lop) |= dataStk[0])
+#define Op_OrXU(lop) (dataStk.drop(), (lop) ^= dataStk[0])
+#define Op_ShLU(lop) (dataStk.drop(), (lop) <<= dataStk[0] & 31)
+#define Op_ShRI(lop) (dataStk.drop(), OpFunc_ShRI(lop, dataStk[0]))
+#define Op_SubU(lop) (dataStk.drop(), (lop) -= dataStk[0])
+
+//
+// OpSet
+//
+#define OpSet(op) \
+   DeclCase(op##_GblArr): \
+      Op_##op(scopeGbl->arrV[*codePtr++][dataStk[1]]); dataStk.drop(); \
+      NextCase(); \
+   DeclCase(op##_GblReg): \
+      Op_##op(scopeGbl->regV[*codePtr++]); \
+      NextCase(); \
+   DeclCase(op##_HubArr): \
+      Op_##op(scopeHub->arrV[*codePtr++][dataStk[1]]); dataStk.drop(); \
+      NextCase(); \
+   DeclCase(op##_HubReg): \
+      Op_##op(scopeHub->regV[*codePtr++]); \
+      NextCase(); \
+   DeclCase(op##_LocArr): \
+      Op_##op(localArr[*codePtr++][dataStk[1]]); dataStk.drop(); \
+      NextCase(); \
+   DeclCase(op##_LocReg): \
+      Op_##op(localReg[*codePtr++]); \
+      NextCase(); \
+   DeclCase(op##_ModArr): \
+      Op_##op((*scopeMod->arrV[*codePtr++])[dataStk[1]]); dataStk.drop(); \
+      NextCase(); \
+   DeclCase(op##_ModReg): \
+      Op_##op(*scopeMod->regV[*codePtr++]); \
+      NextCase()
+
+
+//----------------------------------------------------------------------------|
+// Static Functions                                                           |
+//
+
+namespace ACSVM
+{
+   //
+   // OpFunc_CmpI_GE
+   //
+   static inline void OpFunc_CmpI_GE(Word &lop, Word rop)
+   {
+      lop = static_cast<SWord>(lop) >= static_cast<SWord>(rop);
+   }
+
+   //
+   // OpFunc_CmpI_GT
+   //
+   static inline void OpFunc_CmpI_GT(Word &lop, Word rop)
+   {
+      lop = static_cast<SWord>(lop) > static_cast<SWord>(rop);
+   }
+
+   //
+   // OpFunc_CmpI_LE
+   //
+   static inline void OpFunc_CmpI_LE(Word &lop, Word rop)
+   {
+      lop = static_cast<SWord>(lop) <= static_cast<SWord>(rop);
+   }
+
+   //
+   // OpFunc_CmpI_LT
+   //
+   static inline void OpFunc_CmpI_LT(Word &lop, Word rop)
+   {
+      lop = static_cast<SWord>(lop) < static_cast<SWord>(rop);
+   }
+
+   //
+   // OpFunc_CmpU_EQ
+   //
+   static inline void OpFunc_CmpU_EQ(Word &lop, Word rop)
+   {
+      lop = lop == rop;
+   }
+
+   //
+   // OpFunc_CmpU_NE
+   //
+   static inline void OpFunc_CmpU_NE(Word &lop, Word rop)
+   {
+      lop = lop != rop;
+   }
+
+   //
+   // OpFunc_DivI
+   //
+   static inline void OpFunc_DivI(Word &lop, Word rop)
+   {
+      lop = rop ? static_cast<SWord>(lop) / static_cast<SWord>(rop) : 0;
+   }
+
+   //
+   // OpFunc_DivX
+   //
+   static inline void OpFunc_DivX(Word &lop, Word rop)
+   {
+      if(rop)
+         lop = (SDWord(SWord(lop)) << 16) / SWord(rop);
+      else
+         lop = 0;
+   }
+
+   //
+   // OpFunc_LAnd
+   //
+   static inline void OpFunc_LAnd(Word &lop, Word rop)
+   {
+      lop = lop && rop;
+   }
+
+   //
+   // OpFunc_LOrI
+   //
+   static inline void OpFunc_LOrI(Word &lop, Word rop)
+   {
+      lop = lop || rop;
+   }
+
+   //
+   // OpFunc_ModI
+   //
+   static inline void OpFunc_ModI(Word &lop, Word rop)
+   {
+      lop = rop ? static_cast<SWord>(lop) % static_cast<SWord>(rop) : 0;
+   }
+
+   //
+   // OpFunc_MulX
+   //
+   static inline void OpFunc_MulX(Word &lop, Word rop)
+   {
+      lop = DWord(SDWord(SWord(lop)) * SWord(rop)) >> 16;
+   }
+
+   //
+   // OpFunc_ShRI
+   //
+   static inline void OpFunc_ShRI(Word &lop, Word rop)
+   {
+      // TODO: Implement this without relying on sign-extending shift.
+      lop = static_cast<SWord>(lop) >> (rop & 31);
+   }
+}
+
+
+//----------------------------------------------------------------------------|
+// Extern Functions                                                           |
+//
+
+namespace ACSVM
+{
+   //
+   // Thread::exec
+   //
+   void Thread::exec()
+   {
+      if(delay && --delay)
+         return;
+
+      auto branches = env->branchLimit;
+
+   exec_intr:
+      switch(state.state)
+      {
+      case ThreadState::Inactive: return;
+      case ThreadState::Stopped:  goto thread_stop;
+      case ThreadState::Paused:   return;
+
+      case ThreadState::Running:
+         if(delay)
+            return;
+         break;
+
+      case ThreadState::WaitScrI:
+         if(scopeMap->isScriptActive(scopeMap->findScript(state.data)))
+            return;
+         state = ThreadState::Running;
+         break;
+
+      case ThreadState::WaitScrS:
+         if(scopeMap->isScriptActive(scopeMap->findScript(scopeMap->getString(state.data))))
+            return;
+         state = ThreadState::Running;
+         break;
+
+      case ThreadState::WaitTag:
+         if(!module->env->checkTag(state.type, state.data))
+            return;
+         state = ThreadState::Running;
+         break;
+      }
+
+      #if ACSVM_DynamicGoto
+      static void const *const cases[] =
+      {
+         #define ACSVM_CodeList(name, ...) &&case_Code##name,
+         #include "CodeList.hpp"
+      };
+      #endif
+
+      #if ACSVM_DynamicGoto
+      NextCase();
+      #else
+      next_case: switch(*codePtr++)
+      #endif
+      {
+      DeclCase(Nop):
+         NextCase();
+
+      DeclCase(Kill):
+         module->env->printKill(this, codePtr[0], codePtr[1]);
+         goto thread_stop;
+
+         //================================================
+         // Binary operator codes.
+         //
+
+         OpSet(AddU);
+         OpSet(AndU);
+         OpSet(DivI);
+         OpSet(ModI);
+         OpSet(MulU);
+         OpSet(OrIU);
+         OpSet(OrXU);
+         OpSet(ShLU);
+         OpSet(ShRI);
+         OpSet(SubU);
+
+      DeclCase(AddU): Op_AddU(dataStk[1]); NextCase();
+      DeclCase(AndU): Op_AndU(dataStk[1]); NextCase();
+      DeclCase(CmpI_GE): Op_CmpI_GE(dataStk[1]); NextCase();
+      DeclCase(CmpI_GT): Op_CmpI_GT(dataStk[1]); NextCase();
+      DeclCase(CmpI_LE): Op_CmpI_LE(dataStk[1]); NextCase();
+      DeclCase(CmpI_LT): Op_CmpI_LT(dataStk[1]); NextCase();
+      DeclCase(CmpU_EQ): Op_CmpU_EQ(dataStk[1]); NextCase();
+      DeclCase(CmpU_NE): Op_CmpU_NE(dataStk[1]); NextCase();
+      DeclCase(DivI): Op_DivI(dataStk[1]); NextCase();
+      DeclCase(DivX): Op_DivX(dataStk[1]); NextCase();
+      DeclCase(LAnd): Op_LAnd(dataStk[1]); NextCase();
+      DeclCase(LOrI): Op_LOrI(dataStk[1]); NextCase();
+      DeclCase(ModI): Op_ModI(dataStk[1]); NextCase();
+      DeclCase(MulU): Op_MulU(dataStk[1]); NextCase();
+      DeclCase(MulX): Op_MulX(dataStk[1]); NextCase();
+      DeclCase(OrIU): Op_OrIU(dataStk[1]); NextCase();
+      DeclCase(OrXU): Op_OrXU(dataStk[1]); NextCase();
+      DeclCase(ShLU): Op_ShLU(dataStk[1]); NextCase();
+      DeclCase(ShRI): Op_ShRI(dataStk[1]); NextCase();
+      DeclCase(SubU): Op_SubU(dataStk[1]); NextCase();
+
+         //================================================
+         // Call codes.
+         //
+
+      DeclCase(Call_Lit):
+         {
+            Function *func;
+
+            func = *codePtr < module->functionV.size() ? module->functionV[*codePtr] : nullptr;
+            ++codePtr;
+
+         do_call:
+            if(!func) {BranchTo(0); NextCase();}
+
+            // Reserve stack space.
+            callStk.reserve(CallStkSize);
+            dataStk.reserve(DataStkSize);
+
+            // Push call frame.
+            callStk.push({codePtr, module, scopeMod, localArr.size(), localReg.size()});
+
+            // Apply function data.
+            codePtr      = &func->module->codeV[func->codeIdx];
+            module       = func->module;
+            scopeMod     = scopeMap->getModuleScope(module);
+            localArr.alloc(func->locArrC);
+            localReg.alloc(func->locRegC);
+
+            // Read arguments.
+            dataStk.drop(func->argC);
+            memcpy(&localReg[0], &dataStk[0], func->argC * sizeof(Word));
+
+            NextCase();
+
+      DeclCase(Call_Stk):
+            dataStk.drop();
+            func = env->getFunction(dataStk[0]);
+            goto do_call;
+         }
+
+      DeclCase(CallFunc):
+         {
+            Word argc = *codePtr++;
+            Word func = *codePtr++;
+            dataStk.drop(argc);
+            if(env->callFunc(this, func, &dataStk[0], argc))
+               goto exec_intr;
+         }
+         NextCase();
+
+      DeclCase(CallFunc_Lit):
+         {
+            Word        argc = *codePtr++;
+            Word        func = *codePtr++;
+            Word const *argv =  codePtr;
+            codePtr += argc;
+            if(env->callFunc(this, func, argv, argc))
+               goto exec_intr;
+         }
+         NextCase();
+
+      DeclCase(CallSpec):
+         {
+            Word argc = *codePtr++;
+            Word spec = *codePtr++;
+            dataStk.drop(argc);
+            env->callSpec(this, spec, &dataStk[0], argc);
+         }
+         NextCase();
+
+      DeclCase(CallSpec_Lit):
+         {
+            Word        argc = *codePtr++;
+            Word        spec = *codePtr++;
+            Word const *argv =  codePtr;
+            codePtr += argc;
+            env->callSpec(this, spec, argv, argc);
+         }
+         NextCase();
+
+      DeclCase(CallSpec_R1):
+         {
+            Word argc = *codePtr++;
+            Word spec = *codePtr++;
+            dataStk.drop(argc);
+            dataStk.push(env->callSpec(this, spec, &dataStk[0], argc));
+         }
+         NextCase();
+
+      DeclCase(Retn):
+         // If no call frames left, terminate the thread.
+         if(callStk.empty())
+            goto thread_stop;
+
+         // Apply call frame.
+         codePtr     = callStk[1].codePtr;
+         module      = callStk[1].module;
+         scopeMod    = callStk[1].scopeMod;
+         localArr.free(callStk[1].locArrC);
+         localReg.free(callStk[1].locRegC);
+
+         // Drop call frame.
+         callStk.drop();
+
+         NextCase();
+
+         //================================================
+         // Drop codes.
+         //
+
+         OpSet(Drop);
+
+      DeclCase(Drop_Nul):
+        dataStk.drop();
+        NextCase();
+
+      DeclCase(Drop_ScrRet):
+        dataStk.drop();
+        result = dataStk[0];
+        NextCase();
+
+         //================================================
+         // Jump codes.
+         //
+
+      DeclCase(Jcnd_Lit):
+        if(dataStk[1] == *codePtr++)
+        {
+           dataStk.drop();
+           BranchTo(*codePtr);
+        }
+        else
+           ++codePtr;
+        NextCase();
+
+      DeclCase(Jcnd_Nil):
+         if(dataStk.drop(), dataStk[0])
+            ++codePtr;
+         else
+            BranchTo(*codePtr);
+         NextCase();
+
+      DeclCase(Jcnd_Tab):
+         if(auto jump = module->jumpMapV[*codePtr++].table.find(dataStk[1]))
+         {
+            dataStk.drop();
+            BranchTo(*jump);
+         }
+         NextCase();
+
+      DeclCase(Jcnd_Tru):
+         if(dataStk.drop(), dataStk[0])
+            BranchTo(*codePtr);
+         else
+            ++codePtr;
+         NextCase();
+
+      DeclCase(Jump_Lit):
+        BranchTo(*codePtr);
+        NextCase();
+
+      DeclCase(Jump_Stk):
+         dataStk.drop();
+         BranchTo(dataStk[0] < module->jumpV.size() ? module->jumpV[dataStk[0]].codeIdx : 0);
+         NextCase();
+
+         //================================================
+         // Push codes.
+         //
+
+      DeclCase(Pfun_Lit):
+         if(*codePtr < module->functionV.size())
+            dataStk.push(module->functionV[*codePtr]->idx);
+         else
+            dataStk.push(0);
+         ++codePtr;
+         NextCase();
+
+      DeclCase(Pstr_Stk):
+         if(dataStk[1] < module->stringV.size())
+            dataStk[1] = ~module->stringV[dataStk[1]]->idx;
+         NextCase();
+
+      DeclCase(Push_GblArr): dataStk[1] = scopeGbl->arrV[*codePtr++].find(dataStk[1]); NextCase();
+      DeclCase(Push_GblReg): dataStk.push(scopeGbl->regV[*codePtr++]); NextCase();
+      DeclCase(Push_HubArr): dataStk[1] = scopeHub->arrV[*codePtr++].find(dataStk[1]); NextCase();
+      DeclCase(Push_HubReg): dataStk.push(scopeHub->regV[*codePtr++]); NextCase();
+      DeclCase(Push_Lit):    dataStk.push(*codePtr++); NextCase();
+      DeclCase(Push_LitArr): for(auto i = *codePtr++; i--;) dataStk.push(*codePtr++); NextCase();
+      DeclCase(Push_LocArr): dataStk[1] = localArr[*codePtr++].find(dataStk[1]); NextCase();
+      DeclCase(Push_LocReg): dataStk.push(localReg[*codePtr++]); NextCase();
+      DeclCase(Push_ModArr): dataStk[1] = scopeMod->arrV[*codePtr++]->find(dataStk[1]); NextCase();
+      DeclCase(Push_ModReg): dataStk.push(*scopeMod->regV[*codePtr++]); NextCase();
+
+      DeclCase(Push_StrArs):
+         dataStk.drop();
+         dataStk[1] = scopeMap->getString(dataStk[1])->get(dataStk[0]);
+         NextCase();
+
+         //================================================
+         // Script control codes.
+         //
+
+      DeclCase(ScrDelay):
+         dataStk.drop();
+         delay = dataStk[0];
+         goto exec_intr;
+
+      DeclCase(ScrDelay_Lit):
+         delay = *codePtr++;
+         goto exec_intr;
+
+      DeclCase(ScrHalt):
+         state = ThreadState::Paused;
+         goto exec_intr;
+
+      DeclCase(ScrRestart):
+         BranchTo(script->codeIdx);
+         NextCase();
+
+      DeclCase(ScrTerm):
+         goto thread_stop;
+
+      DeclCase(ScrWaitI):
+         dataStk.drop();
+         state = {ThreadState::WaitScrI, dataStk[0]};
+         goto exec_intr;
+
+      DeclCase(ScrWaitI_Lit):
+         state = {ThreadState::WaitScrI, *codePtr++};
+         goto exec_intr;
+
+      DeclCase(ScrWaitS):
+         dataStk.drop();
+         state = {ThreadState::WaitScrS, dataStk[0]};
+         goto exec_intr;
+
+      DeclCase(ScrWaitS_Lit):
+         state = {ThreadState::WaitScrS, *codePtr++};
+         goto exec_intr;
+
+         //================================================
+         // Stack control codes.
+         //
+
+      DeclCase(Copy):
+         {auto temp = dataStk[1]; dataStk.push(temp);}
+         NextCase();
+
+      DeclCase(Swap):
+         std::swap(dataStk[2], dataStk[1]);
+         NextCase();
+
+         //================================================
+         // Unary operator codes.
+         //
+
+         OpSet(DecU);
+         OpSet(IncU);
+
+      DeclCase(InvU):
+         dataStk[1] = ~dataStk[1];
+         NextCase();
+
+      DeclCase(NegI):
+         dataStk[1] = ~dataStk[1] + 1;
+         NextCase();
+
+      DeclCase(NotU):
+         dataStk[1] = !dataStk[1];
+         NextCase();
+      }
+
+   thread_stop:
+      stop();
+   }
+}
+
+// EOF
+
diff --git a/src/acs/vm/ACSVM/Tracer.cpp b/src/acs/vm/ACSVM/Tracer.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..7e8e7e3b744fffec5dec94a4b50a6a1e07d69e08
--- /dev/null
+++ b/src/acs/vm/ACSVM/Tracer.cpp
@@ -0,0 +1,636 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2015 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// Tracer classes.
+//
+//-----------------------------------------------------------------------------
+
+#include "Tracer.hpp"
+
+#include "BinaryIO.hpp"
+#include "Code.hpp"
+#include "CodeData.hpp"
+#include "Environment.hpp"
+#include "Error.hpp"
+#include "Function.hpp"
+#include "Jump.hpp"
+#include "Module.hpp"
+#include "Script.hpp"
+
+
+//----------------------------------------------------------------------------|
+// Extern Functions                                                           |
+//
+
+namespace ACSVM
+{
+   //
+   // TracerACS0 constructor
+   //
+   TracerACS0::TracerACS0(Environment *env_, Byte const *data_,
+      std::size_t size_, bool compressed_) :
+      env       {env_},
+      codeFound {new Byte[size_]{}},
+      codeIndex {new Word[size_]{}},
+      codeC     {0},
+      jumpC     {0},
+      jumpMapC  {0},
+      data      {data_},
+      size      {size_},
+      compressed{compressed_}
+   {
+   }
+
+   //
+   // TracerACS0 destructor
+   //
+   TracerACS0::~TracerACS0()
+   {
+   }
+
+   //
+   // TracerACS0::getArgBytes
+   //
+   std::size_t TracerACS0::getArgBytes(CodeDataACS0 const *opData, std::size_t iter)
+   {
+      std::size_t argBytes;
+
+      switch(opData->code)
+      {
+      case CodeACS0::Push_LitArrB:
+         if(size - iter < 1) throw ReadError();
+
+         return data[iter] + 1;
+
+      case CodeACS0::Jcnd_Tab:
+         // Calculate alignment.
+         argBytes = ((iter + 3) & ~static_cast<std::size_t>(3)) - iter;
+         if(size - iter < argBytes) throw ReadError();
+
+         // Read the number of cases.
+         if(size - iter - argBytes < 4) throw ReadError();
+         argBytes += ReadLE4(data + iter + argBytes) * 8 + 4;
+
+         return argBytes;
+
+      default:
+         argBytes = 0;
+         for(char const *s = opData->args; *s; ++s) switch(*s)
+         {
+         case 'B': argBytes += 1; break;
+         case 'H': argBytes += 2; break;
+         case 'W': argBytes += 4; break;
+         case 'b': argBytes += compressed ? 1 : 4; break;
+         case 'h': argBytes += compressed ? 2 : 4; break;
+         }
+         return argBytes;
+      }
+   }
+
+   //
+   // TracerACS0::readCallFunc
+   //
+   std::pair<Word /*argc*/, Word /*func*/> TracerACS0::readCallFunc(std::size_t iter)
+   {
+      Word argc, func;
+      if(compressed)
+      {
+         argc = ReadLE1(data + iter + 0);
+         func = ReadLE2(data + iter + 1);
+      }
+      else
+      {
+         argc = ReadLE4(data + iter + 0);
+         func = ReadLE4(data + iter + 4);
+      }
+      return {argc, func};
+   }
+
+   //
+   // TracerACS0::readOpACS0
+   //
+   std::tuple<
+      Word                 /*opCode*/,
+      CodeDataACS0 const * /*opData*/,
+      std::size_t          /*opSize*/>
+   TracerACS0::readOpACS0(std::size_t iter)
+   {
+      if(compressed)
+      {
+         if(size - iter < 1) throw ReadError();
+
+         std::size_t opSize = 1;
+         Word        opCode = data[iter];
+
+         if(opCode >= 240)
+         {
+            if(size - iter < 2) throw ReadError();
+            ++opSize;
+            opCode = 240 + ((opCode - 240) << 8) + data[iter + 1];
+         }
+
+         return std::make_tuple(opCode, env->findCodeDataACS0(opCode), opSize);
+      }
+      else
+      {
+         if(size - iter < 4) throw ReadError();
+
+         Word opCode = ReadLE4(data + iter);
+
+         return std::make_tuple(opCode, env->findCodeDataACS0(opCode), 4);
+      }
+   }
+
+   //
+   // TracerACS0::setFound
+   //
+   bool TracerACS0::setFound(std::size_t first, std::size_t last)
+   {
+      Byte *begin = &codeFound[first];
+      Byte *end   = &codeFound[last];
+
+      std::size_t found = 0;
+
+      for(Byte *itr = begin; itr != end; ++itr)
+         found += *itr;
+
+      if(found)
+      {
+         if(found != last - first) throw ReadError();
+         return false;
+      }
+
+      for(Byte *itr = begin; itr != end; ++itr)
+         *itr = true;
+
+      return true;
+   }
+
+   //
+   // TracerACS0::trace
+   //
+   void TracerACS0::trace(Module *module)
+   {
+      // Add Kill to catch branches to zero.
+      codeC += 1 + env->getCodeData(Code::Kill)->argc;
+
+      // Trace from entry points.
+
+      for(Function *&func : module->functionV)
+         if(func && func->module == module) trace(func->codeIdx);
+
+      for(Jump &jump : module->jumpV)
+         trace(jump.codeIdx);
+
+      for(Script &scr : module->scriptV)
+         trace(scr.codeIdx);
+
+      // Add Kill to catch execution past end.
+      codeC += 1 + env->getCodeData(Code::Kill)->argc;
+   }
+
+   //
+   // TracerACS0::trace
+   //
+   void TracerACS0::trace(std::size_t iter)
+   {
+      for(std::size_t next;; iter = next)
+      {
+         // If at the end of the file, terminate tracer. Reaching here will
+         // result in a Kill, but the bytecode is otherwise well formed.
+         if(iter == size) return;
+
+         // Whereas if iter is out of bounds, bytecode is malformed.
+         if(iter > size) throw ReadError();
+
+         // Read op.
+         CodeDataACS0 const *opData;
+         std::size_t         opSize;
+         std::tie(std::ignore, opData, opSize) = readOpACS0(iter);
+
+         // If no translation available, terminate trace.
+         if(!opData)
+         {
+            // Mark as found, so that the translator generates a KILL.
+            setFound(iter, iter + opSize);
+            codeC += 1 + env->getCodeData(Code::Kill)->argc;
+            return;
+         }
+
+         std::size_t opSizeFull = opSize + getArgBytes(opData, iter + opSize);
+
+         // If this op goes out of bounds, bytecode is malformed.
+         if(size - iter < opSizeFull) throw ReadError();
+
+         next = iter + opSizeFull;
+
+         // If this op already found, terminate trace.
+         if(!setFound(iter, next))
+            return;
+
+         // Get data for translated op.
+         CodeData const *opTran = env->getCodeData(opData->transCode);
+
+         // Count internal size of op.
+         switch(opData->code)
+         {
+            // -> Call(F) Drop_Nul()
+         case CodeACS0::Call_Nul:
+            codeC += 3;
+            break;
+
+         case CodeACS0::CallSpec_1L:
+         case CodeACS0::CallSpec_2L:
+         case CodeACS0::CallSpec_3L:
+         case CodeACS0::CallSpec_4L:
+         case CodeACS0::CallSpec_5L:
+         case CodeACS0::CallSpec_6L:
+         case CodeACS0::CallSpec_7L:
+         case CodeACS0::CallSpec_8L:
+         case CodeACS0::CallSpec_9L:
+         case CodeACS0::CallSpec_10L:
+         case CodeACS0::CallSpec_1LB:
+         case CodeACS0::CallSpec_2LB:
+         case CodeACS0::CallSpec_3LB:
+         case CodeACS0::CallSpec_4LB:
+         case CodeACS0::CallSpec_5LB:
+         case CodeACS0::CallSpec_6LB:
+         case CodeACS0::CallSpec_7LB:
+         case CodeACS0::CallSpec_8LB:
+         case CodeACS0::CallSpec_9LB:
+         case CodeACS0::CallSpec_10LB:
+         case CodeACS0::Push_Lit2B:
+         case CodeACS0::Push_Lit3B:
+         case CodeACS0::Push_Lit4B:
+         case CodeACS0::Push_Lit5B:
+         case CodeACS0::Push_Lit6B:
+         case CodeACS0::Push_Lit7B:
+         case CodeACS0::Push_Lit8B:
+         case CodeACS0::Push_Lit9B:
+         case CodeACS0::Push_Lit10B:
+            codeC += opData->argc + 2;
+            break;
+
+         case CodeACS0::Push_LitArrB:
+            codeC += data[iter + opSize] + 2;
+            break;
+
+            // -> Push_Lit(0) Retn()
+         case CodeACS0::Retn_Nul:
+            codeC += 3;
+            break;
+
+         case CodeACS0::CallFunc:
+            {
+               Word argc, func;
+               std::tie(argc, func) = readCallFunc(iter + opSize);
+
+               FuncDataACS0 const *opFunc = env->findFuncDataACS0(func);
+
+               if(!opFunc)
+               {
+                  codeC += 1 + env->getCodeData(Code::Kill)->argc;
+                  return;
+               }
+
+               opTran = env->getCodeData(opFunc->getTransCode(argc));
+            }
+            /* FALLTHRU */
+         default:
+            if(opTran->code == Code::CallFunc_Lit)
+               codeC += opData->argc + 1 + 2;
+            else
+               codeC += opTran->argc + 1;
+
+            if(opTran->code == Code::Kill)
+               return;
+
+            break;
+         }
+
+         // Special handling for branching ops.
+         switch(opData->code)
+         {
+         case CodeACS0::Jcnd_Nil:
+         case CodeACS0::Jcnd_Tru:
+            ++jumpC;
+            trace(ReadLE4(data + iter + opSize));
+            break;
+
+         case CodeACS0::Jcnd_Lit:
+            ++jumpC;
+            trace(ReadLE4(data + iter + opSize + 4));
+            break;
+
+         case CodeACS0::Jcnd_Tab:
+            {
+               std::size_t count, jumpIter;
+
+               jumpIter = (iter + opSize + 3) & ~static_cast<std::size_t>(3);
+               count = ReadLE4(data + jumpIter); jumpIter += 4;
+
+               ++jumpMapC;
+
+               // Trace all of the jump targets.
+               for(; count--; jumpIter += 8)
+                  trace(ReadLE4(data + jumpIter + 4));
+            }
+            break;
+
+         case CodeACS0::Jump_Lit:
+            ++jumpC;
+            next = ReadLE4(data + iter + opSize);
+            break;
+
+         case CodeACS0::Jump_Stk:
+            // The target of this jump will get traced when the dynamic jump
+            // targets get traced.
+            return;
+
+         case CodeACS0::Retn_Stk:
+         case CodeACS0::Retn_Nul:
+         case CodeACS0::ScrTerm:
+            return;
+
+         default:
+            break;
+         }
+      }
+   }
+
+   //
+   // TracerACS0::translate
+   //
+   void TracerACS0::translate(Module *module)
+   {
+      std::unique_ptr<Word*[]> jumps{new uint32_t *[jumpC]};
+
+      Word  *codeItr    = module->codeV.data();
+      Word **jumpItr    = jumps.get();
+      auto   jumpMapItr = module->jumpMapV.data();
+
+      // Add Kill to catch branches to zero.
+      *codeItr++ = static_cast<Word>(Code::Kill);
+      *codeItr++ = static_cast<Word>(KillType::OutOfBounds);
+      *codeItr++ = 0;
+
+      for(std::size_t iter = 0, next; iter != size; iter = next)
+      {
+         // If no code at this index, skip it.
+         if(!codeFound[iter])
+         {
+            next = iter + 1;
+            continue;
+         }
+
+         // Record jump target.
+         codeIndex[iter] = codeItr - module->codeV.data();
+
+         // Read op.
+         Word                opCode;
+         CodeDataACS0 const *opData;
+         std::size_t         opSize;
+         std::tie(opCode, opData, opSize) = readOpACS0(iter);
+
+         // If no translation available, generate Kill.
+         if(!opData)
+         {
+            *codeItr++ = static_cast<Word>(Code::Kill);
+            *codeItr++ = static_cast<Word>(KillType::UnknownCode);
+            *codeItr++ = opCode;
+            next = iter + opSize;
+            continue;
+         }
+
+         // Calculate next index.
+         next = iter + opSize + getArgBytes(opData, iter + opSize);
+
+         // Get data for translated op.
+         CodeData const *opTran = env->getCodeData(opData->transCode);
+
+         // Generate internal op.
+         switch(opData->code)
+         {
+         case CodeACS0::Call_Nul:
+            *codeItr++ = static_cast<Word>(opData->transCode);
+            if(compressed)
+               *codeItr++ = ReadLE1(data + iter + opSize);
+            else
+               *codeItr++ = ReadLE4(data + iter + opSize);
+            *codeItr++ = static_cast<Word>(Code::Drop_Nul);
+            break;
+
+         case CodeACS0::CallSpec_1:
+         case CodeACS0::CallSpec_2:
+         case CodeACS0::CallSpec_3:
+         case CodeACS0::CallSpec_4:
+         case CodeACS0::CallSpec_5:
+         case CodeACS0::CallSpec_5Ex:
+         case CodeACS0::CallSpec_6:
+         case CodeACS0::CallSpec_7:
+         case CodeACS0::CallSpec_8:
+         case CodeACS0::CallSpec_9:
+         case CodeACS0::CallSpec_10:
+         case CodeACS0::CallSpec_5R1:
+         case CodeACS0::CallSpec_10R1:
+            *codeItr++ = static_cast<Word>(opData->transCode);
+            *codeItr++ = opData->stackArgC;
+            goto trans_args;
+
+         case CodeACS0::CallSpec_1L:
+         case CodeACS0::CallSpec_1LB:
+         case CodeACS0::CallSpec_2L:
+         case CodeACS0::CallSpec_2LB:
+         case CodeACS0::CallSpec_3L:
+         case CodeACS0::CallSpec_3LB:
+         case CodeACS0::CallSpec_4L:
+         case CodeACS0::CallSpec_4LB:
+         case CodeACS0::CallSpec_5L:
+         case CodeACS0::CallSpec_5LB:
+         case CodeACS0::CallSpec_6L:
+         case CodeACS0::CallSpec_6LB:
+         case CodeACS0::CallSpec_7L:
+         case CodeACS0::CallSpec_7LB:
+         case CodeACS0::CallSpec_8L:
+         case CodeACS0::CallSpec_8LB:
+         case CodeACS0::CallSpec_9L:
+         case CodeACS0::CallSpec_9LB:
+         case CodeACS0::CallSpec_10L:
+         case CodeACS0::CallSpec_10LB:
+            *codeItr++ = static_cast<Word>(opData->transCode);
+            *codeItr++ = opData->argc - 1;
+            goto trans_args;
+
+         case CodeACS0::Jcnd_Tab:
+            {
+               std::size_t count, jumpIter;
+
+               jumpIter = (iter + opSize + 3) & ~static_cast<std::size_t>(3);
+               count = ReadLE4(data + jumpIter); jumpIter += 4;
+
+               *codeItr++ = static_cast<Word>(opData->transCode);
+               *codeItr++ = jumpMapItr - module->jumpMapV.data();
+
+               (jumpMapItr++)->loadJumps(data + jumpIter, count);
+            }
+            break;
+
+         case CodeACS0::Push_LitArrB:
+            *codeItr++ = static_cast<Word>(opData->transCode);
+            iter += opSize;
+            for(std::size_t n = *codeItr++ = data[iter++]; n--;)
+               *codeItr++ = data[iter++];
+            break;
+
+         case CodeACS0::Push_Lit2B:
+         case CodeACS0::Push_Lit3B:
+         case CodeACS0::Push_Lit4B:
+         case CodeACS0::Push_Lit5B:
+         case CodeACS0::Push_Lit6B:
+         case CodeACS0::Push_Lit7B:
+         case CodeACS0::Push_Lit8B:
+         case CodeACS0::Push_Lit9B:
+         case CodeACS0::Push_Lit10B:
+            *codeItr++ = static_cast<Word>(opData->transCode);
+            *codeItr++ = opData->argc;
+            goto trans_args;
+
+         case CodeACS0::Retn_Nul:
+            *codeItr++ = static_cast<Word>(Code::Push_Lit);
+            *codeItr++ = static_cast<Word>(0);
+            *codeItr++ = static_cast<Word>(opData->transCode);
+            break;
+
+         case CodeACS0::CallFunc:
+            {
+               Word argc, func;
+               std::tie(argc, func) = readCallFunc(iter + opSize);
+
+               FuncDataACS0 const *opFunc = env->findFuncDataACS0(func);
+
+               if(!opFunc)
+               {
+                  *codeItr++ = static_cast<Word>(Code::Kill);
+                  *codeItr++ = static_cast<Word>(KillType::UnknownFunc);
+                  *codeItr++ = func;
+                  continue;
+               }
+
+               opTran = env->getCodeData(opFunc->getTransCode(argc));
+
+               *codeItr++ = static_cast<Word>(opTran->code);
+               if(opTran->code == Code::Kill)
+               {
+                  *codeItr++ = static_cast<Word>(KillType::UnknownFunc);
+                  *codeItr++ = func;
+                  continue;
+               }
+               else if(opTran->code == Code::CallFunc)
+               {
+                  *codeItr++ = argc;
+                  *codeItr++ = opFunc->transFunc;
+               }
+            }
+            break;
+
+         default:
+            *codeItr++ = static_cast<Word>(opData->transCode);
+            if(opTran->code == Code::Kill)
+            {
+               *codeItr++ = static_cast<Word>(KillType::UnknownCode);
+               *codeItr++ = opCode;
+               continue;
+            }
+            else if(opTran->code == Code::CallFunc)
+            {
+               *codeItr++ = opData->stackArgC;
+               *codeItr++ = opData->transFunc;
+            }
+            else if(opTran->code == Code::CallFunc_Lit)
+            {
+               *codeItr++ = opData->argc;
+               *codeItr++ = opData->transFunc;
+            }
+
+         trans_args:
+            // Convert arguments.
+            iter += opSize;
+            for(char const *a = opData->args; *a; ++a) switch(*a)
+            {
+            case 'B': *codeItr++ = ReadLE1(data + iter); iter += 1; break;
+            case 'H': *codeItr++ = ReadLE2(data + iter); iter += 2; break;
+            case 'W': *codeItr++ = ReadLE4(data + iter); iter += 4; break;
+
+            case 'J':
+               *jumpItr++ = codeItr - 1;
+               break;
+
+            case 'S':
+               if(*(codeItr - 1) < module->stringV.size())
+                  *(codeItr - 1) = ~module->stringV[*(codeItr - 1)]->idx;
+               break;
+
+            case 'b':
+               if(compressed)
+                  {*codeItr++ = ReadLE1(data + iter); iter += 1;}
+               else
+                  {*codeItr++ = ReadLE4(data + iter); iter += 4;}
+               break;
+
+            case 'h':
+               if(compressed)
+                  {*codeItr++ = ReadLE2(data + iter); iter += 2;}
+               else
+                  {*codeItr++ = ReadLE4(data + iter); iter += 4;}
+               break;
+            }
+
+            break;
+         }
+      }
+
+      // Add Kill to catch execution past end.
+      *codeItr++ = static_cast<Word>(Code::Kill);
+      *codeItr++ = static_cast<Word>(KillType::OutOfBounds);
+      *codeItr++ = 1;
+
+      // Translate jumps. Has to be done after code in order to jump forward.
+      while(jumpItr != jumps.get())
+      {
+         codeItr = *--jumpItr;
+
+         if(*codeItr < size)
+            *codeItr = codeIndex[*codeItr];
+         else
+            *codeItr = 0;
+      }
+
+      // Translate entry points.
+
+      for(Function *&func : module->functionV)
+      {
+         if(func && func->module == module)
+            func->codeIdx = func->codeIdx < size ? codeIndex[func->codeIdx] : 0;
+      }
+
+      for(Jump &jump : module->jumpV)
+         jump.codeIdx = jump.codeIdx < size ? codeIndex[jump.codeIdx] : 0;
+
+      for(JumpMap &jumpMap : module->jumpMapV)
+      {
+         for(auto &jump : jumpMap.table)
+            jump.val = jump.val < size ? codeIndex[jump.val] : 0;
+      }
+
+      for(Script &scr : module->scriptV)
+         scr.codeIdx = scr.codeIdx < size ? codeIndex[scr.codeIdx] : 0;
+   }
+}
+
+// EOF
+
diff --git a/src/acs/vm/ACSVM/Tracer.hpp b/src/acs/vm/ACSVM/Tracer.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..d0918946a1501796244d01514ff390751a5fe423
--- /dev/null
+++ b/src/acs/vm/ACSVM/Tracer.hpp
@@ -0,0 +1,76 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2015 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// Tracer classes.
+//
+//-----------------------------------------------------------------------------
+
+#ifndef ACSVM__Tracer_H__
+#define ACSVM__Tracer_H__
+
+#include "Types.hpp"
+
+#include <memory>
+
+
+//----------------------------------------------------------------------------|
+// Types                                                                      |
+//
+
+namespace ACSVM
+{
+   //
+   // TracerACS0
+   //
+   // Traces input bytecode in ACS0 format for code paths, and then
+   // translates discovered codes.
+   //
+   class TracerACS0
+   {
+   public:
+      TracerACS0(Environment *env, Byte const *data, std::size_t size, bool compressed);
+      ~TracerACS0();
+
+      void trace(Module *module);
+
+      void translate(Module *module);
+
+      Environment *env;
+
+      std::unique_ptr<Byte[]> codeFound;
+      std::unique_ptr<Word[]> codeIndex;
+      std::size_t             codeC;
+
+      std::size_t jumpC;
+
+      std::size_t jumpMapC;
+
+   private:
+      std::size_t getArgBytes(CodeDataACS0 const *opData, std::size_t iter);
+
+      std::pair<Word /*argc*/, Word /*func*/> readCallFunc(std::size_t iter);
+
+      std::tuple<
+         Word                 /*opCode*/,
+         CodeDataACS0 const * /*opData*/,
+         std::size_t          /*opSize*/>
+      readOpACS0(std::size_t iter);
+
+      bool setFound(std::size_t first, std::size_t last);
+
+      void trace(std::size_t iter);
+
+      // Bytecode information.
+      Byte const *data;
+      std::size_t size;
+      bool        compressed;
+   };
+}
+
+#endif//ACSVM__Tracer_H__
+
diff --git a/src/acs/vm/ACSVM/Types.hpp b/src/acs/vm/ACSVM/Types.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..8da5445d570d176a0897071b8e86b9d1cfa70871
--- /dev/null
+++ b/src/acs/vm/ACSVM/Types.hpp
@@ -0,0 +1,81 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2015-2017 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// Common typedefs and class forward declarations.
+//
+//-----------------------------------------------------------------------------
+
+#ifndef ACSVM__Types_H__
+#define ACSVM__Types_H__
+
+#include <cinttypes>
+#include <cstddef>
+
+
+//----------------------------------------------------------------------------|
+// Types                                                                      |
+//
+
+namespace ACSVM
+{
+   // Host platform byte.
+   // Should this be uint8_t? Short answer: No.
+   // Long answer: char is the smallest addressable unit in the implementation.
+   // That is, it is by definition the byte of the target platform. And it is
+   // required to be at least 8 bits wide, which is the only real requirement
+   // for loading ACS bytecode.
+   // Furthermore, uint8_t is only defined if the implementation has a type
+   // with exactly 8 data bits and no padding bits. So even if you wanted to
+   // unilaterally declare bytes to be 8 bits, the only type that can possibly
+   // satisfy uint8_t is unsigned char. If CHAR_BIT is not 8, then there can be
+   // no uint8_t.
+   using Byte = unsigned char;
+
+   using DWord = std::uint64_t;
+   using SDWord = std::int64_t;
+   using SWord = std::int32_t;
+   using Word = std::uint32_t;
+
+   enum class Code;
+   enum class CodeACS0;
+   enum class Func;
+   enum class FuncACS0;
+   enum class InitTag;
+   enum class Signature : std::uint32_t;
+   class Array;
+   class ArrayInit;
+   class CodeData;
+   class CodeDataACS0;
+   class Environment;
+   class FuncDataACS0;
+   class Function;
+   class GlobalScope;
+   class HubScope;
+   class Jump;
+   class JumpMap;
+   class MapScope;
+   class Module;
+   class ModuleName;
+   class ModuleScope;
+   class PrintBuf;
+   class ScopeID;
+   class Script;
+   class ScriptAction;
+   class ScriptName;
+   class Serial;
+   class String;
+   class Thread;
+   class ThreadInfo;
+   class ThreadState;
+   class WordInit;
+
+   using CallFunc = bool (*)(Thread *thread, Word const *argv, Word argc);
+}
+
+#endif//ACSVM__Types_H__
+
diff --git a/src/acs/vm/ACSVM/Vector.hpp b/src/acs/vm/ACSVM/Vector.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..966bc6ea9a45f805bd99d164d84359671c5327c4
--- /dev/null
+++ b/src/acs/vm/ACSVM/Vector.hpp
@@ -0,0 +1,140 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2015-2016 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// Vector class.
+//
+//-----------------------------------------------------------------------------
+
+#ifndef ACSVM__Vector_H__
+#define ACSVM__Vector_H__
+
+#include "Types.hpp"
+
+#include <new>
+#include <utility>
+
+
+//----------------------------------------------------------------------------|
+// Types                                                                      |
+//
+
+namespace ACSVM
+{
+   //
+   // Vector
+   //
+   // Runtime sized array.
+   //
+   template<typename T>
+   class Vector
+   {
+   public:
+      using const_iterator = T const *;
+      using iterator       = T *;
+      using size_type      = std::size_t;
+
+
+      Vector() : dataV{nullptr}, dataC{0} {}
+      Vector(Vector<T> const &) = delete;
+      Vector(Vector<T> &&v) : dataV{v.dataV}, dataC{v.dataC}
+         {v.dataV = nullptr; v.dataC = 0;}
+      Vector(size_type count) : dataV{nullptr}, dataC{0} {alloc(count);}
+
+      Vector(T const *v, size_type c)
+      {
+         dataC = c;
+         dataV = static_cast<T *>(::operator new(sizeof(T) * dataC));
+
+         for(T *itr = dataV, *last = itr + dataC; itr != last; ++itr)
+            new(itr) T{*v++};
+      }
+
+      ~Vector() {free();}
+
+      T &operator [] (size_type i) {return dataV[i];}
+
+      Vector<T> &operator = (Vector<T> &&v) {swap(v); return *this;}
+
+      //
+      // alloc
+      //
+      template<typename... Args>
+      void alloc(size_type count, Args const &...args)
+      {
+         if(dataV) free();
+
+         dataC = count;
+         dataV = static_cast<T *>(::operator new(sizeof(T) * dataC));
+
+         for(T *itr = dataV, *last = itr + dataC; itr != last; ++itr)
+            new(itr) T{args...};
+      }
+
+      // begin
+            iterator begin()       {return dataV;}
+      const_iterator begin() const {return dataV;}
+
+      // data
+      T *data() {return dataV;}
+
+      // end
+            iterator end()       {return dataV + dataC;}
+      const_iterator end() const {return dataV + dataC;}
+
+      //
+      // free
+      //
+      void free()
+      {
+         if(!dataV) return;
+
+         for(T *itr = dataV + dataC; itr != dataV;)
+            (--itr)->~T();
+
+         ::operator delete(dataV);
+         dataV = nullptr;
+         dataC = 0;
+      }
+
+      //
+      // realloc
+      //
+      template<typename... Args>
+      void realloc(size_type count, Args const &...args)
+      {
+         if(count == dataC) return;
+
+         Vector<T> old{std::move(*this)};
+
+         dataC = count;
+         dataV = static_cast<T *>(::operator new(sizeof(T) * dataC));
+
+         T *itr = begin(), *last = end(), *oldItr = old.begin();
+         T *mid = count > old.size() ? dataV + old.size() : last;
+
+         while(itr != mid)
+            new(itr++) T{std::move(*oldItr++)};
+         while(itr != last)
+            new(itr++) T{args...};
+      }
+
+      // size
+      size_type size() const {return dataC;}
+
+      // swap
+      void swap(Vector<T> &v)
+         {std::swap(dataV, v.dataV); std::swap(dataC, v.dataC);}
+
+   private:
+      T        *dataV;
+      size_type dataC;
+   };
+}
+
+#endif//ACSVM__Vector_H__
+
diff --git a/src/acs/vm/CMakeLists.txt b/src/acs/vm/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..3116012c67ec1e89d47c5e3afbb3e17ad523c889
--- /dev/null
+++ b/src/acs/vm/CMakeLists.txt
@@ -0,0 +1,193 @@
+##-----------------------------------------------------------------------------
+##
+## Copyright (C) 2015-2016 David Hill
+##
+## See COPYING for license information.
+##
+##-----------------------------------------------------------------------------
+##
+## Root CMake file.
+##
+##-----------------------------------------------------------------------------
+
+cmake_minimum_required(VERSION 2.6)
+
+cmake_policy(SET CMP0017 NEW)
+
+project(acsvm)
+
+include(CheckCCompilerFlag)
+include(CheckCXXCompilerFlag)
+
+
+##----------------------------------------------------------------------------|
+## Functions                                                                  |
+##
+
+##
+## ACSVM_INSTALL_EXE
+##
+function(ACSVM_INSTALL_EXE name)
+   if(ACSVM_INSTALL_EXE)
+      install(TARGETS ${name}
+         RUNTIME DESTINATION bin
+         LIBRARY DESTINATION lib
+         ARCHIVE DESTINATION lib
+      )
+   endif()
+endfunction()
+
+##
+## ACSVM_INSTALL_LIB
+##
+function(ACSVM_INSTALL_LIB name)
+   if(ACSVM_INSTALL_LIB)
+      if(ACSVM_INSTALL_API)
+         install(TARGETS ${name}
+            RUNTIME DESTINATION bin
+            LIBRARY DESTINATION lib
+            ARCHIVE DESTINATION lib
+         )
+      elseif(ACSVM_SHARED)
+         install(TARGETS ${name}
+            RUNTIME DESTINATION bin
+            LIBRARY DESTINATION lib
+         )
+      endif()
+   endif()
+endfunction()
+
+##
+## ACSVM_TRY_C_FLAG
+##
+function(ACSVM_TRY_C_FLAG flag name)
+   CHECK_C_COMPILER_FLAG(${flag} ACSVM_C_FLAG_${name})
+
+   if(ACSVM_C_FLAG_${name})
+      set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${flag}" PARENT_SCOPE)
+   endif()
+endfunction()
+
+##
+## ACSVM_TRY_CXX_FLAG
+##
+function(ACSVM_TRY_CXX_FLAG flag name)
+   CHECK_CXX_COMPILER_FLAG(${flag} ACSVM_CXX_FLAG_${name})
+
+   if(ACSVM_CXX_FLAG_${name})
+      set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${flag}" PARENT_SCOPE)
+   endif()
+endfunction()
+
+
+##----------------------------------------------------------------------------|
+## Environment Detection                                                      |
+##
+
+set(ACSVM_SHARED_DEFAULT ON)
+
+if(NOT ACSVM_NOFLAGS)
+   if(CMAKE_C_COMPILER_ID STREQUAL "GNU" OR CMAKE_C_COMPILER_ID STREQUAL "Clang")
+      ACSVM_TRY_C_FLAG(-Wall    Wall)
+      ACSVM_TRY_C_FLAG(-Wextra  Wextra)
+
+      ACSVM_TRY_C_FLAG(-std=c11 STD_C)
+   endif()
+
+   if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
+      ACSVM_TRY_CXX_FLAG(-Wall    Wall)
+      ACSVM_TRY_CXX_FLAG(-Wextra  Wextra)
+
+      ACSVM_TRY_CXX_FLAG(-std=c++11 STD_CXX)
+   endif()
+endif()
+
+if(MSVC)
+   # Disable shared by default, as the source does not contain the needed
+   # declaration annotations to make that work under MSVC.
+   set(ACSVM_SHARED_DEFAULT OFF)
+endif()
+
+
+##----------------------------------------------------------------------------|
+## Variables                                                                  |
+##
+
+##
+## ACSVM_INSTALL_API
+##
+if(NOT DEFINED ACSVM_INSTALL_API)
+   set(ACSVM_INSTALL_API ON CACHE BOOL "Install ACSVM headers.")
+endif()
+
+##
+## ACSVM_INSTALL_DOC
+##
+if(NOT DEFINED ACSVM_INSTALL_DOC)
+   set(ACSVM_INSTALL_DOC ON CACHE BOOL "Install ACSVM documentation.")
+endif()
+
+##
+## ACSVM_INSTALL_EXE
+##
+if(NOT DEFINED ACSVM_INSTALL_EXE)
+   set(ACSVM_INSTALL_EXE ON CACHE BOOL "Install ACSVM executables.")
+endif()
+
+##
+## ACSVM_INSTALL_LIB
+##
+if(NOT DEFINED ACSVM_INSTALL_LIB)
+   set(ACSVM_INSTALL_LIB ON CACHE BOOL "Install ACSVM libraries.")
+endif()
+
+##
+## ACSVM_SHARED
+##
+## If true (or equiavalent), libraries will be built as SHARED. Otherwise,
+## they are built as STATIC.
+##
+if(NOT DEFINED ACSVM_SHARED)
+   set(ACSVM_SHARED ${ACSVM_SHARED_DEFAULT} CACHE BOOL
+      "Build libraries as shared objects.")
+endif()
+
+##
+## ACSVM_SHARED_DECL
+##
+## Used internally for convenience in add_library commands.
+##
+if(ACSVM_SHARED)
+   set(ACSVM_SHARED_DECL SHARED)
+else()
+   set(ACSVM_SHARED_DECL STATIC)
+endif()
+
+
+##----------------------------------------------------------------------------|
+## Environment Configuration                                                  |
+##
+
+include_directories(.)
+
+
+##----------------------------------------------------------------------------|
+## Targets                                                                    |
+##
+
+add_subdirectory(ACSVM)
+
+if(EXISTS "${CMAKE_SOURCE_DIR}/CAPI")
+   add_subdirectory(CAPI)
+endif()
+
+if(EXISTS "${CMAKE_SOURCE_DIR}/Exec")
+   add_subdirectory(Exec)
+endif()
+
+if(EXISTS "${CMAKE_SOURCE_DIR}/Util")
+   add_subdirectory(Util)
+endif()
+
+## EOF
+
diff --git a/src/acs/vm/COPYING b/src/acs/vm/COPYING
new file mode 100644
index 0000000000000000000000000000000000000000..4362b49151d7b34ef83b3067a8f9c9f877d72a0e
--- /dev/null
+++ b/src/acs/vm/COPYING
@@ -0,0 +1,502 @@
+                  GNU LESSER GENERAL PUBLIC LICENSE
+                       Version 2.1, February 1999
+
+ Copyright (C) 1991, 1999 Free Software Foundation, Inc.
+ 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+[This is the first released version of the Lesser GPL.  It also counts
+ as the successor of the GNU Library Public License, version 2, hence
+ the version number 2.1.]
+
+                            Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+Licenses are intended to guarantee your freedom to share and change
+free software--to make sure the software is free for all its users.
+
+  This license, the Lesser General Public License, applies to some
+specially designated software packages--typically libraries--of the
+Free Software Foundation and other authors who decide to use it.  You
+can use it too, but we suggest you first think carefully about whether
+this license or the ordinary General Public License is the better
+strategy to use in any particular case, based on the explanations below.
+
+  When we speak of free software, we are referring to freedom of use,
+not price.  Our General Public Licenses are designed to make sure that
+you have the freedom to distribute copies of free software (and charge
+for this service if you wish); that you receive source code or can get
+it if you want it; that you can change the software and use pieces of
+it in new free programs; and that you are informed that you can do
+these things.
+
+  To protect your rights, we need to make restrictions that forbid
+distributors to deny you these rights or to ask you to surrender these
+rights.  These restrictions translate to certain responsibilities for
+you if you distribute copies of the library or if you modify it.
+
+  For example, if you distribute copies of the library, whether gratis
+or for a fee, you must give the recipients all the rights that we gave
+you.  You must make sure that they, too, receive or can get the source
+code.  If you link other code with the library, you must provide
+complete object files to the recipients, so that they can relink them
+with the library after making changes to the library and recompiling
+it.  And you must show them these terms so they know their rights.
+
+  We protect your rights with a two-step method: (1) we copyright the
+library, and (2) we offer you this license, which gives you legal
+permission to copy, distribute and/or modify the library.
+
+  To protect each distributor, we want to make it very clear that
+there is no warranty for the free library.  Also, if the library is
+modified by someone else and passed on, the recipients should know
+that what they have is not the original version, so that the original
+author's reputation will not be affected by problems that might be
+introduced by others.
+
+  Finally, software patents pose a constant threat to the existence of
+any free program.  We wish to make sure that a company cannot
+effectively restrict the users of a free program by obtaining a
+restrictive license from a patent holder.  Therefore, we insist that
+any patent license obtained for a version of the library must be
+consistent with the full freedom of use specified in this license.
+
+  Most GNU software, including some libraries, is covered by the
+ordinary GNU General Public License.  This license, the GNU Lesser
+General Public License, applies to certain designated libraries, and
+is quite different from the ordinary General Public License.  We use
+this license for certain libraries in order to permit linking those
+libraries into non-free programs.
+
+  When a program is linked with a library, whether statically or using
+a shared library, the combination of the two is legally speaking a
+combined work, a derivative of the original library.  The ordinary
+General Public License therefore permits such linking only if the
+entire combination fits its criteria of freedom.  The Lesser General
+Public License permits more lax criteria for linking other code with
+the library.
+
+  We call this license the "Lesser" General Public License because it
+does Less to protect the user's freedom than the ordinary General
+Public License.  It also provides other free software developers Less
+of an advantage over competing non-free programs.  These disadvantages
+are the reason we use the ordinary General Public License for many
+libraries.  However, the Lesser license provides advantages in certain
+special circumstances.
+
+  For example, on rare occasions, there may be a special need to
+encourage the widest possible use of a certain library, so that it becomes
+a de-facto standard.  To achieve this, non-free programs must be
+allowed to use the library.  A more frequent case is that a free
+library does the same job as widely used non-free libraries.  In this
+case, there is little to gain by limiting the free library to free
+software only, so we use the Lesser General Public License.
+
+  In other cases, permission to use a particular library in non-free
+programs enables a greater number of people to use a large body of
+free software.  For example, permission to use the GNU C Library in
+non-free programs enables many more people to use the whole GNU
+operating system, as well as its variant, the GNU/Linux operating
+system.
+
+  Although the Lesser General Public License is Less protective of the
+users' freedom, it does ensure that the user of a program that is
+linked with the Library has the freedom and the wherewithal to run
+that program using a modified version of the Library.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.  Pay close attention to the difference between a
+"work based on the library" and a "work that uses the library".  The
+former contains code derived from the library, whereas the latter must
+be combined with the library in order to run.
+
+                  GNU LESSER GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License Agreement applies to any software library or other
+program which contains a notice placed by the copyright holder or
+other authorized party saying it may be distributed under the terms of
+this Lesser General Public License (also called "this License").
+Each licensee is addressed as "you".
+
+  A "library" means a collection of software functions and/or data
+prepared so as to be conveniently linked with application programs
+(which use some of those functions and data) to form executables.
+
+  The "Library", below, refers to any such software library or work
+which has been distributed under these terms.  A "work based on the
+Library" means either the Library or any derivative work under
+copyright law: that is to say, a work containing the Library or a
+portion of it, either verbatim or with modifications and/or translated
+straightforwardly into another language.  (Hereinafter, translation is
+included without limitation in the term "modification".)
+
+  "Source code" for a work means the preferred form of the work for
+making modifications to it.  For a library, complete source code means
+all the source code for all modules it contains, plus any associated
+interface definition files, plus the scripts used to control compilation
+and installation of the library.
+
+  Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running a program using the Library is not restricted, and output from
+such a program is covered only if its contents constitute a work based
+on the Library (independent of the use of the Library in a tool for
+writing it).  Whether that is true depends on what the Library does
+and what the program that uses the Library does.
+
+  1. You may copy and distribute verbatim copies of the Library's
+complete source code as you receive it, in any medium, provided that
+you conspicuously and appropriately publish on each copy an
+appropriate copyright notice and disclaimer of warranty; keep intact
+all the notices that refer to this License and to the absence of any
+warranty; and distribute a copy of this License along with the
+Library.
+
+  You may charge a fee for the physical act of transferring a copy,
+and you may at your option offer warranty protection in exchange for a
+fee.
+
+  2. You may modify your copy or copies of the Library or any portion
+of it, thus forming a work based on the Library, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) The modified work must itself be a software library.
+
+    b) You must cause the files modified to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    c) You must cause the whole of the work to be licensed at no
+    charge to all third parties under the terms of this License.
+
+    d) If a facility in the modified Library refers to a function or a
+    table of data to be supplied by an application program that uses
+    the facility, other than as an argument passed when the facility
+    is invoked, then you must make a good faith effort to ensure that,
+    in the event an application does not supply such function or
+    table, the facility still operates, and performs whatever part of
+    its purpose remains meaningful.
+
+    (For example, a function in a library to compute square roots has
+    a purpose that is entirely well-defined independent of the
+    application.  Therefore, Subsection 2d requires that any
+    application-supplied function or table used by this function must
+    be optional: if the application does not supply it, the square
+    root function must still compute square roots.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Library,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Library, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote
+it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Library.
+
+In addition, mere aggregation of another work not based on the Library
+with the Library (or with a work based on the Library) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may opt to apply the terms of the ordinary GNU General Public
+License instead of this License to a given copy of the Library.  To do
+this, you must alter all the notices that refer to this License, so
+that they refer to the ordinary GNU General Public License, version 2,
+instead of to this License.  (If a newer version than version 2 of the
+ordinary GNU General Public License has appeared, then you can specify
+that version instead if you wish.)  Do not make any other change in
+these notices.
+
+  Once this change is made in a given copy, it is irreversible for
+that copy, so the ordinary GNU General Public License applies to all
+subsequent copies and derivative works made from that copy.
+
+  This option is useful when you wish to copy part of the code of
+the Library into a program that is not a library.
+
+  4. You may copy and distribute the Library (or a portion or
+derivative of it, under Section 2) in object code or executable form
+under the terms of Sections 1 and 2 above provided that you accompany
+it with the complete corresponding machine-readable source code, which
+must be distributed under the terms of Sections 1 and 2 above on a
+medium customarily used for software interchange.
+
+  If distribution of object code is made by offering access to copy
+from a designated place, then offering equivalent access to copy the
+source code from the same place satisfies the requirement to
+distribute the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  5. A program that contains no derivative of any portion of the
+Library, but is designed to work with the Library by being compiled or
+linked with it, is called a "work that uses the Library".  Such a
+work, in isolation, is not a derivative work of the Library, and
+therefore falls outside the scope of this License.
+
+  However, linking a "work that uses the Library" with the Library
+creates an executable that is a derivative of the Library (because it
+contains portions of the Library), rather than a "work that uses the
+library".  The executable is therefore covered by this License.
+Section 6 states terms for distribution of such executables.
+
+  When a "work that uses the Library" uses material from a header file
+that is part of the Library, the object code for the work may be a
+derivative work of the Library even though the source code is not.
+Whether this is true is especially significant if the work can be
+linked without the Library, or if the work is itself a library.  The
+threshold for this to be true is not precisely defined by law.
+
+  If such an object file uses only numerical parameters, data
+structure layouts and accessors, and small macros and small inline
+functions (ten lines or less in length), then the use of the object
+file is unrestricted, regardless of whether it is legally a derivative
+work.  (Executables containing this object code plus portions of the
+Library will still fall under Section 6.)
+
+  Otherwise, if the work is a derivative of the Library, you may
+distribute the object code for the work under the terms of Section 6.
+Any executables containing that work also fall under Section 6,
+whether or not they are linked directly with the Library itself.
+
+  6. As an exception to the Sections above, you may also combine or
+link a "work that uses the Library" with the Library to produce a
+work containing portions of the Library, and distribute that work
+under terms of your choice, provided that the terms permit
+modification of the work for the customer's own use and reverse
+engineering for debugging such modifications.
+
+  You must give prominent notice with each copy of the work that the
+Library is used in it and that the Library and its use are covered by
+this License.  You must supply a copy of this License.  If the work
+during execution displays copyright notices, you must include the
+copyright notice for the Library among them, as well as a reference
+directing the user to the copy of this License.  Also, you must do one
+of these things:
+
+    a) Accompany the work with the complete corresponding
+    machine-readable source code for the Library including whatever
+    changes were used in the work (which must be distributed under
+    Sections 1 and 2 above); and, if the work is an executable linked
+    with the Library, with the complete machine-readable "work that
+    uses the Library", as object code and/or source code, so that the
+    user can modify the Library and then relink to produce a modified
+    executable containing the modified Library.  (It is understood
+    that the user who changes the contents of definitions files in the
+    Library will not necessarily be able to recompile the application
+    to use the modified definitions.)
+
+    b) Use a suitable shared library mechanism for linking with the
+    Library.  A suitable mechanism is one that (1) uses at run time a
+    copy of the library already present on the user's computer system,
+    rather than copying library functions into the executable, and (2)
+    will operate properly with a modified version of the library, if
+    the user installs one, as long as the modified version is
+    interface-compatible with the version that the work was made with.
+
+    c) Accompany the work with a written offer, valid for at
+    least three years, to give the same user the materials
+    specified in Subsection 6a, above, for a charge no more
+    than the cost of performing this distribution.
+
+    d) If distribution of the work is made by offering access to copy
+    from a designated place, offer equivalent access to copy the above
+    specified materials from the same place.
+
+    e) Verify that the user has already received a copy of these
+    materials or that you have already sent this user a copy.
+
+  For an executable, the required form of the "work that uses the
+Library" must include any data and utility programs needed for
+reproducing the executable from it.  However, as a special exception,
+the materials to be distributed need not include anything that is
+normally distributed (in either source or binary form) with the major
+components (compiler, kernel, and so on) of the operating system on
+which the executable runs, unless that component itself accompanies
+the executable.
+
+  It may happen that this requirement contradicts the license
+restrictions of other proprietary libraries that do not normally
+accompany the operating system.  Such a contradiction means you cannot
+use both them and the Library together in an executable that you
+distribute.
+
+  7. You may place library facilities that are a work based on the
+Library side-by-side in a single library together with other library
+facilities not covered by this License, and distribute such a combined
+library, provided that the separate distribution of the work based on
+the Library and of the other library facilities is otherwise
+permitted, and provided that you do these two things:
+
+    a) Accompany the combined library with a copy of the same work
+    based on the Library, uncombined with any other library
+    facilities.  This must be distributed under the terms of the
+    Sections above.
+
+    b) Give prominent notice with the combined library of the fact
+    that part of it is a work based on the Library, and explaining
+    where to find the accompanying uncombined form of the same work.
+
+  8. You may not copy, modify, sublicense, link with, or distribute
+the Library except as expressly provided under this License.  Any
+attempt otherwise to copy, modify, sublicense, link with, or
+distribute the Library is void, and will automatically terminate your
+rights under this License.  However, parties who have received copies,
+or rights, from you under this License will not have their licenses
+terminated so long as such parties remain in full compliance.
+
+  9. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Library or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Library (or any work based on the
+Library), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Library or works based on it.
+
+  10. Each time you redistribute the Library (or any work based on the
+Library), the recipient automatically receives a license from the
+original licensor to copy, distribute, link with or modify the Library
+subject to these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties with
+this License.
+
+  11. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Library at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Library by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Library.
+
+If any portion of this section is held invalid or unenforceable under any
+particular circumstance, the balance of the section is intended to apply,
+and the section as a whole is intended to apply in other circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  12. If the distribution and/or use of the Library is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Library under this License may add
+an explicit geographical distribution limitation excluding those countries,
+so that distribution is permitted only in or among countries not thus
+excluded.  In such case, this License incorporates the limitation as if
+written in the body of this License.
+
+  13. The Free Software Foundation may publish revised and/or new
+versions of the Lesser General Public License from time to time.
+Such new versions will be similar in spirit to the present version,
+but may differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Library
+specifies a version number of this License which applies to it and
+"any later version", you have the option of following the terms and
+conditions either of that version or of any later version published by
+the Free Software Foundation.  If the Library does not specify a
+license version number, you may choose any version ever published by
+the Free Software Foundation.
+
+  14. If you wish to incorporate parts of the Library into other free
+programs whose distribution conditions are incompatible with these,
+write to the author to ask for permission.  For software which is
+copyrighted by the Free Software Foundation, write to the Free
+Software Foundation; we sometimes make exceptions for this.  Our
+decision will be guided by the two goals of preserving the free status
+of all derivatives of our free software and of promoting the sharing
+and reuse of software generally.
+
+                            NO WARRANTY
+
+  15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
+WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
+EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
+OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
+KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
+LIBRARY IS WITH YOU.  SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
+THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
+WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
+AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
+FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
+CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
+LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
+RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
+FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
+SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGES.
+
+                     END OF TERMS AND CONDITIONS
+
+           How to Apply These Terms to Your New Libraries
+
+  If you develop a new library, and you want it to be of the greatest
+possible use to the public, we recommend making it free software that
+everyone can redistribute and change.  You can do so by permitting
+redistribution under these terms (or, alternatively, under the terms of the
+ordinary General Public License).
+
+  To apply these terms, attach the following notices to the library.  It is
+safest to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least the
+"copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the library's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Lesser General Public
+    License as published by the Free Software Foundation; either
+    version 2.1 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Lesser General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public
+    License along with this library; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+
+Also add information on how to contact you by electronic and paper mail.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the library, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the
+  library `Frob' (a library for tweaking knobs) written by James Random Hacker.
+
+  <signature of Ty Coon>, 1 April 1990
+  Ty Coon, President of Vice
+
+That's all there is to it!
diff --git a/src/acs/vm/README b/src/acs/vm/README
new file mode 100644
index 0000000000000000000000000000000000000000..bb24555639c4cd2db9be2627d5f9ee7b5f34145b
--- /dev/null
+++ b/src/acs/vm/README
@@ -0,0 +1,121 @@
+ACS Virtual Machine (ACSVM)
+
+ACS VM library and standalone interpreter. Intended to be suitable for use in
+Doom-based video game engines to implement Hexen ACS or ZDoom ACS bytecode
+execution. It is focused on being usable for implementing existing extensions
+and new functions (whether through new instructions or CallFunc indexes) while
+having performance suitable to the complex ACS-based mods that have come into
+existence.
+
+
+===============================================================================
+Integrating ACSVM
+===============================================================================
+
+Although it can be used as an external library, ACSVM is also written so that
+it can be integrated directly into the repository of projects using it without
+needing to change any of the ACSVM files.
+
+It is enough to copy ACSVM's root into the root of the target repository, such
+that acsvm/CMakeLists.txt exists. Only the subdirectories of desired components
+(usually just ACSVM) need to be included. The CMakeLists.txt will automatically
+disable the omitted components.
+
+In your root CMakeLists.txt, all that is needed is:
+  set(ACSVM_NOFLAGS ON)
+  set(ACSVM_SHARED OFF)
+  add_subdirectory(acsvm)
+And the enabled components (again, usually just acsvm) will be available for
+use in target_link_libraries.
+
+
+===============================================================================
+Usage Overview
+===============================================================================
+
+===========================================================
+Getting Started
+===========================================================
+
+To use ACSVM, you will need to define a class that inherits from
+ACSVM::Environment. By overriding the various virtuals you can configure the
+different aspects of ACS loading and interpretation. But the absolute minimal
+usage only requires overriding loadModule:
+  class Env : public ACSVM::Environment
+  {
+  protected:
+    virtual void loadModule(ACSVM::Module *module);
+  };
+
+Which is implemented by using the module's name to locate the corresponding
+bytecode and passing that to module->readBytecode. The default behavior of
+getModuleName is to just set the ModuleName's string. This can be used to
+implement bytecode directly from files:
+  void Env::loadModule(ACSVM::Module *module)
+  {
+    std::ifstream in{module->name.s->str,
+       std::ios_base::in | std::ios_base::binary};
+
+    if(!in) throw ACSVM::ReadError("file open failure");
+
+    std::vector<ACSVM::Byte> data;
+
+    for(int c; c = in.get(), in;)
+      data.push_back(c);
+
+    module->readBytecode(data.data(), data.size());
+  }
+In a Doom engine, this would most likely use lumps, instead. Either by doing
+the lookup in loadModule, or by overriding getModuleName to turn the input
+string into a lump number.
+
+To actually initialize the environment and load some modules, you can use:
+  void EnvInit(Environment &env, char const *const *namev, std::size_t namec)
+  {
+    // Load modules.
+    std::vector<ACSVM::Module *> modules;
+    for(std::size_t i = 1; i < namec; ++i)
+      modules.push_back(env.getModule(env.getModuleName(namev[i])));
+
+    // Create and activate scopes.
+    ACSVM::GlobalScope *global = env.getGlobalScope(0);  global->active = true;
+    ACSVM::HubScope    *hub    = global->getHubScope(0); hub   ->active = true;
+    ACSVM::MapScope    *map    = hub->getMapScope(0);    map   ->active = true;
+
+    // Register modules with map scope.
+    map->addModules(modules.data(), modules.size());
+
+    // Start Open scripts.
+    map->scriptStartType(1, {});
+  }
+
+And then a simple interpreter loop:
+   while(env.hasActiveThread())
+   {
+      std::chrono::duration<double> rate{1.0 / 35};
+      auto time = std::chrono::steady_clock::now() + rate;
+
+      env.exec();
+
+      std::this_thread::sleep_until(time);
+   }
+Note that if you already have a game loop, you only need to call env.exec once
+per simulation frame.
+
+Finally, you will need to register instruction and callfunc functions to
+actually interface with the larger environment. At the least, it is useful to
+implement the EndPrint (86) instruction:
+  bool CF_EndPrint(ACSVM::Thread *thread, ACSVM::Word const *, ACSVM::Word)
+  {
+    std::cout << thread->printBuf.data() << '\n';
+    thread->printBuf.drop();
+    return false;
+  }
+
+  Environment::Environment()
+  {
+    addCodeDataACS0(86, {"", 0, addCallFunc(CF_EndPrint)});
+  }
+Most of the other ACS printing logic is already handled by ACSVM, so this is
+enough to display simple Print messages.
+
diff --git a/src/acs/vm/Util/CMakeLists.txt b/src/acs/vm/Util/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..081e078d2c053ccfaf5482e6e50f78c16804342b
--- /dev/null
+++ b/src/acs/vm/Util/CMakeLists.txt
@@ -0,0 +1,38 @@
+##-----------------------------------------------------------------------------
+##
+## Copyright (C) 2015 David Hill
+##
+## See COPYING for license information.
+##
+##-----------------------------------------------------------------------------
+##
+## CMake file for acsvm-util.
+##
+##-----------------------------------------------------------------------------
+
+
+##----------------------------------------------------------------------------|
+## Environment Configuration                                                  |
+##
+
+include_directories(.)
+
+
+##----------------------------------------------------------------------------|
+## Targets                                                                    |
+##
+
+##
+## acsvm-capi
+##
+add_library(acsvm-util ${ACSVM_SHARED_DECL}
+   Floats.cpp
+   Floats.hpp
+)
+
+target_link_libraries(acsvm-util acsvm)
+
+ACSVM_INSTALL_LIB(acsvm-util)
+
+## EOF
+
diff --git a/src/acs/vm/Util/Floats.cpp b/src/acs/vm/Util/Floats.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..7436e6ea4eb39b1b9ace76fce436d36271364015
--- /dev/null
+++ b/src/acs/vm/Util/Floats.cpp
@@ -0,0 +1,89 @@
+//----------------------------------------------------------------------------
+//
+// Copyright (C) 2015 David Hill
+//
+// See COPYING for license information.
+//
+//----------------------------------------------------------------------------
+//
+// Floating-point utilities.
+//
+//----------------------------------------------------------------------------
+
+#include "Floats.hpp"
+
+#include "ACSVM/Thread.hpp"
+
+#include <cstdio>
+
+
+//----------------------------------------------------------------------------|
+// Macros                                                                     |
+//
+
+//
+// DoubleOp
+//
+#define DoubleOp(name, op) \
+   bool CF_##name##F_W2(Thread *thread, Word const *argV, Word) \
+   { \
+      double l = WordsToFloat<double, 2>({{argV[0], argV[1]}}); \
+      double r = WordsToFloat<double, 2>({{argV[2], argV[3]}}); \
+      for(auto w : FloatToWords<2>(l op r)) thread->dataStk.push(w); \
+      return false; \
+   }
+
+//
+// FloatOp
+//
+#define FloatOp(name, op) \
+   bool CF_##name##F_W1(Thread *thread, Word const *argV, Word) \
+   { \
+      float l = WordsToFloat<float, 1>({{argV[0]}}); \
+      float r = WordsToFloat<float, 1>({{argV[1]}}); \
+      for(auto w : FloatToWords<1>(l op r)) thread->dataStk.push(w); \
+      return false; \
+   }
+
+
+//----------------------------------------------------------------------------|
+// Extern Functions                                                           |
+//
+
+namespace ACSVM
+{
+   FloatOp(Add, +);
+   FloatOp(Div, /);
+   FloatOp(Mul, *);
+   FloatOp(Sub, -);
+
+   DoubleOp(Add, +);
+   DoubleOp(Div, /);
+   DoubleOp(Mul, *);
+   DoubleOp(Sub, -);
+
+   //
+   // void PrintDouble(double f)
+   //
+   bool CF_PrintDouble(Thread *thread, Word const *argV, Word)
+   {
+      double f = WordsToFloat<double, 2>({{argV[0], argV[1]}});
+      thread->printBuf.reserve(std::snprintf(nullptr, 0, "%f", f));
+      thread->printBuf.format("%f", f);
+      return false;
+   }
+
+   //
+   // void PrintFloat(float f)
+   //
+   bool CF_PrintFloat(Thread *thread, Word const *argV, Word)
+   {
+      float f = WordsToFloat<float, 1>({{argV[0]}});
+      thread->printBuf.reserve(std::snprintf(nullptr, 0, "%f", f));
+      thread->printBuf.format("%f", f);
+      return false;
+   }
+}
+
+// EOF
+
diff --git a/src/acs/vm/Util/Floats.hpp b/src/acs/vm/Util/Floats.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..94cd9b25ebb6b3a1a82af7d9aec6cc3413315442
--- /dev/null
+++ b/src/acs/vm/Util/Floats.hpp
@@ -0,0 +1,186 @@
+//----------------------------------------------------------------------------
+//
+// Copyright (C) 2015 David Hill
+//
+// See COPYING for license information.
+//
+//----------------------------------------------------------------------------
+//
+// Floating-point utilities.
+//
+//----------------------------------------------------------------------------
+
+#ifndef ACSVM__Util__Floats_H__
+#define ACSVM__Util__Floats_H__
+
+#include "../ACSVM/Types.hpp"
+
+#include <array>
+#include <cmath>
+
+
+//----------------------------------------------------------------------------|
+// Extern Functions                                                           |
+//
+
+namespace ACSVM
+{
+   bool CF_AddF_W1(Thread *thread, Word const *argV, Word argC);
+   bool CF_DivF_W1(Thread *thread, Word const *argV, Word argC);
+   bool CF_MulF_W1(Thread *thread, Word const *argV, Word argC);
+   bool CF_SubF_W1(Thread *thread, Word const *argV, Word argC);
+   bool CF_AddF_W2(Thread *thread, Word const *argV, Word argC);
+   bool CF_DivF_W2(Thread *thread, Word const *argV, Word argC);
+   bool CF_MulF_W2(Thread *thread, Word const *argV, Word argC);
+   bool CF_SubF_W2(Thread *thread, Word const *argV, Word argC);
+
+   bool CF_PrintDouble(Thread *thread, Word const *argV, Word argC);
+   bool CF_PrintFloat(Thread *thread, Word const *argV, Word argC);
+
+   //
+   // FloatExpBitDefault
+   //
+   template<std::size_t N>
+   constexpr std::size_t FloatExpBitDefault();
+
+   template<> constexpr std::size_t FloatExpBitDefault<1>() {return  8;}
+   template<> constexpr std::size_t FloatExpBitDefault<2>() {return 11;}
+   template<> constexpr std::size_t FloatExpBitDefault<4>() {return 15;}
+
+   //
+   // FloatToWords
+   //
+   template<std::size_t N, std::size_t ExpBit = FloatExpBitDefault<N>(), typename FltT>
+   std::array<Word, N> FloatToWords(FltT const &f)
+   {
+      static_assert(N >= 1, "N must be at least 1.");
+      static_assert(ExpBit >= 2, "ExpBit must be at least 2.");
+      static_assert(ExpBit <= 30, "ExpBit must be at most 30.");
+
+
+      constexpr int ExpMax = (1 << (ExpBit - 1)) - 1;
+      constexpr int ExpMin = -ExpMax - 1;
+      constexpr int ExpOff = ExpMax - 1;
+
+
+      std::array<Word, N> w{};
+      Word &wHi = std::get<N - 1>(w);
+      Word &wLo = std::get<0>(w);
+
+      // Convert sign.
+      bool sigRaw = std::signbit(f);
+      wHi = static_cast<Word>(sigRaw) << 31;
+
+      // Convert special values.
+      switch(std::fpclassify(f))
+      {
+      case FP_INFINITE:
+         // Convert to +/-INFINITY.
+         wHi |= (0xFFFFFFFFu << (31 - ExpBit)) & 0x7FFFFFFF;
+         return w;
+
+      case FP_NAN:
+         // Convert to NAN.
+         wHi |= 0x7FFFFFFF;
+         for(auto itr = &wLo, end = &wHi; itr != end; ++itr)
+            *itr = 0xFFFFFFFF;
+         return w;
+
+      case FP_SUBNORMAL:
+         // TODO: Subnormals.
+      case FP_ZERO:
+         // Convert to +/-0.
+         return w;
+      }
+
+      int  expRaw = 0;
+      FltT manRaw = std::ldexp(std::frexp(std::fabs(f), &expRaw), N * 32 - ExpBit);
+
+      // Check for exponent overflow.
+      if(expRaw > ExpMax)
+      {
+         // Overflow to +/-INFINITY.
+         wHi |= (0xFFFFFFFFu << (32 - ExpBit)) & 0x7FFFFFFF;
+         return w;
+      }
+
+      // Check for exponent underflow.
+      if(expRaw < ExpMin)
+      {
+         // Underflow to +/-0.
+         return w;
+      }
+
+      // Convert exponent.
+      wHi |= static_cast<Word>(expRaw + ExpOff) << (32 - ExpBit - 1);
+
+      // Convert mantissa.
+      for(int i = 0, e = N - 1; i != e; ++i)
+         w[i] = static_cast<Word>(std::fmod(std::ldexp(manRaw, -i * 32), 4294967296.0));
+      wHi |= static_cast<Word>(std::ldexp(manRaw, -static_cast<int>(N - 1) * 32))
+         & ~(0xFFFFFFFFu << (31 - ExpBit));
+
+      return w;
+   }
+
+   //
+   // WordsToFloat
+   //
+   template<typename FltT, std::size_t N, std::size_t ExpBit = FloatExpBitDefault<N>()>
+   FltT WordsToFloat(std::array<Word, N> const &w)
+   {
+      static_assert(N >= 1, "N must be at least 1.");
+      static_assert(ExpBit >= 2, "ExpBit must be at least 2.");
+      static_assert(ExpBit <= 30, "ExpBit must be at most 30.");
+
+
+      constexpr int ExpMax = (1 << (ExpBit - 1)) - 1;
+      constexpr int ExpMin = -ExpMax - 1;
+      constexpr int ExpOff = ExpMax;
+
+      constexpr Word ManMask = 0xFFFFFFFFu >> (ExpBit + 1);
+
+
+      Word const &wHi = std::get<N - 1>(w);
+      Word const &wLo = std::get<0>(w);
+
+      bool sig = !!(wHi & 0x80000000);
+      int  exp = static_cast<int>((wHi & 0x7FFFFFFF) >> (31 - ExpBit)) - ExpOff;
+
+      // INFINITY or NAN.
+      if(exp > ExpMax)
+      {
+         // Check for NAN.
+         for(auto itr = &wLo, end = &wHi; itr != end; ++itr)
+            if(*itr) return NAN;
+         if(wHi & ManMask) return NAN;
+
+         return sig ? -INFINITY : +INFINITY;
+      }
+
+      // Zero or subnormal.
+      if(exp < ExpMin)
+      {
+         // TODO: Subnormals.
+
+         return sig ? -0.0f : +0.0f;
+      }
+
+      // Convert mantissa.
+      FltT f = 0;
+      for(auto itr = &wHi, end = &wLo; itr != end;)
+         f = ldexp(f + *--itr, -32);
+      f = ldexp(f + (wHi & ManMask) + (ManMask + 1), -static_cast<int>(31 - ExpBit));
+
+      // Convert exponent.
+      f = ldexp(f, exp);
+
+      // Convert sign.
+      f = sig ? -f : +f;
+
+      return f;
+   }
+}
+
+#endif//ACSVM__Util__Floats_H__
+
diff --git a/src/acs/vm/doc/ACSVM.txt b/src/acs/vm/doc/ACSVM.txt
new file mode 100644
index 0000000000000000000000000000000000000000..17837859be145e38bc0e15f07dff41c1865ec0a9
--- /dev/null
+++ b/src/acs/vm/doc/ACSVM.txt
@@ -0,0 +1,1212 @@
+###############################################################################
+ACSVM Library Specification
+###############################################################################
+
+===============================================================================
+Types <ACSVM/ACSVM/Types.hpp>
+===============================================================================
+
+Synopsis:
+  using Byte = unsigned char;
+
+  using DWord = std::uint64_t;
+  using SDWord = std::int64_t;
+  using SWord = std::int32_t;
+  using Word = std::uint32_t;
+
+  using CallFunc = bool (*)(Thread *thread, Word const *argv, Word argc);
+
+===============================================================================
+Deferred Actions <ACSVM/ACSVM/Action.hpp>
+===============================================================================
+
+===========================================================
+ACSVM::ScopeID
+===========================================================
+
+Synopsis:
+  #include <ACSVM/ACSVM/Action.hpp>
+  class ScopeID
+  {
+  public:
+    ScopeID() = default;
+    ScopeID(Word global, Word hub, Word map);
+
+    bool operator == (ScopeID const &id) const;
+    bool operator != (ScopeID const &id) const;
+
+    Word global;
+    Word hub;
+    Word map;
+  };
+
+===============================================================================
+Arrays <ACSVM/ACSVM/Array.hpp>
+===============================================================================
+
+===========================================================
+ACSVM::Array
+===========================================================
+
+Synopsis:
+  #include <ACSVM/ACSVM/Array.hpp>
+  class Array
+  {
+  public:
+    Array();
+    Array(Array const &) = delete;
+    Array(Array &&array);
+    ~Array();
+
+    Word &operator [] (Word idx);
+
+    void clear();
+
+    Word find(Word idx) const;
+
+    void lockStrings(Environment *env) const;
+
+    void unlockStrings(Environment *env) const;
+  };
+
+===============================================================================
+Codes <ACSVM/ACSVM/Code.hpp>
+===============================================================================
+
+===========================================================
+ACSVM::Code
+===========================================================
+
+Synopsis:
+  #include <ACSVM/ACSVM/Code.hpp>
+  enum class Code
+  {
+      /* ... */
+      None
+  };
+
+===========================================================
+ACSVM::Func
+===========================================================
+
+Synopsis:
+  #include <ACSVM/ACSVM/Code.hpp>
+  enum class Func
+  {
+    /* ... */
+    None
+  };
+
+===========================================================
+ACSVM::KillType
+===========================================================
+
+Synopsis:
+  #include <ACSVM/ACSVM/Code.hpp>
+  enum class KillType
+  {
+    None,
+    OutOfBounds,
+    UnknownCode,
+    UnknownFunc,
+    BranchLimit,
+  };
+
+===============================================================================
+Code Data <ACSVM/ACSVM/CodeData.hpp>
+===============================================================================
+
+===========================================================
+ACSVM::CodeDataACS0
+===========================================================
+
+Synopsis:
+  #include <ACSVM/ACSVM/CodeData.hpp>
+  class CodeDataACS0
+  {
+  public:
+    CodeDataACS0(char const *args, Code transCode, Word stackArgC,
+      Word transFunc = 0);
+    CodeDataACS0(char const *args, Word stackArgC, Word transFunc);
+
+    char const *args;
+    std::size_t argc;
+
+    Word stackArgC;
+
+    Code transCode;
+
+    Word transFunc;
+  };
+
+===========================================================
+ACSVM::FuncDataACS0
+===========================================================
+
+Synopsis:
+  #include <ACSVM/ACSVM/CodeData.hpp>
+  class FuncDataACS0
+  {
+  public:
+    using TransCode = std::pair<Word, Code>;
+
+
+    FuncDataACS0(FuncDataACS0 const &data);
+    FuncDataACS0(FuncDataACS0 &&data);
+    FuncDataACS0(Word transFunc);
+    FuncDataACS0(Word transFunc, std::initializer_list<TransCode> transCodes);
+    ~FuncDataACS0();
+
+    FuncDataACS0 &operator = (FuncDataACS0 const &) = delete;
+    FuncDataACS0 &operator = (FuncDataACS0 &&data);
+
+    Word transFunc;
+  };
+
+===============================================================================
+Environment <ACSVM/ACSVM/Environment.hpp>
+===============================================================================
+
+===========================================================
+ACSVM::Environment
+===========================================================
+
+Synopsis:
+  #include <ACSVM/ACSVM/Environment.hpp>
+  class Environment
+  {
+  public:
+    Environment();
+    virtual ~Environment();
+
+    Word addCallFunc(CallFunc func);
+
+    void addCodeDataACS0(Word code, CodeDataACS0 &&data);
+
+    void addFuncDataACS0(Word func, FuncDataACS0 &&data);
+
+    virtual bool checkLock(Thread *thread, Word lock, bool door);
+
+    virtual bool checkTag(Word type, Word tag);
+
+    void collectStrings();
+
+    virtual void exec();
+
+    void freeGlobalScope(GlobalScope *scope);
+
+    void freeModule(Module *module);
+
+    GlobalScope *getGlobalScope(Word id);
+
+    Module *getModule(ModuleName const &name);
+
+    ModuleName getModuleName(char const *str);
+    virtual ModuleName getModuleName(char const *str, std::size_t len);
+
+    String *getString(Word idx);
+    String *getString(char const *first, char const *last);
+    String *getString(char const *str);
+    String *getString(char const *str, std::size_t len);
+    String *getString(StringData const *data);
+
+    bool hasActiveThread() const;
+
+    virtual void loadState(Serial &in);
+
+    virtual void printArray(PrintBuf &buf, Array const &array, Word index,
+      Word limit);
+
+    virtual void printKill(Thread *thread, Word type, Word data);
+
+    virtual ModuleName readModuleName(Serial &in) const;
+
+    String *readString(Serial &in) const;
+
+    virtual void refStrings();
+
+    virtual void resetStrings();
+
+    virtual void saveState(Serial &out) const;
+
+    virtual void writeModuleName(Serial &out, ModuleName const &name) const;
+
+    void writeString(Serial &out, String const *in) const;
+
+    StringTable stringTable;
+
+    Word scriptLocRegC;
+
+
+    static void PrintArrayChar(PrintBuf &buf, Array const &array, Word index,
+      Word limit);
+
+    static void PrintArrayUTF8(PrintBuf &buf, Array const &array, Word index,
+      Word limit);
+
+    static constexpr Word ScriptLocRegCDefault;
+
+  protected:
+    virtual Thread *allocThread();
+
+    virtual Word callSpecImpl(Thread *thread, Word spec, Word const *argV,
+      Word argC);
+
+    virtual void loadModule(Module *module) = 0;
+  };
+
+Description:
+  Represents an execution environment.
+
+-----------------------------------------------------------
+ACSVM::Environment::Environment
+-----------------------------------------------------------
+
+Synopsis:
+  Environment();
+
+Description:
+  Constructs the Environment object.
+
+-----------------------------------------------------------
+ACSVM::Environment::~Environment
+-----------------------------------------------------------
+
+Synopsis:
+  ~Environment();
+
+Description:
+  Destructs the Environment object.
+
+-----------------------------------------------------------
+ACSVM::Environment::addCallFunc
+-----------------------------------------------------------
+
+Synopsis:
+  Word addCallFunc(CallFunc func);
+
+Description:
+  Adds a function callback for scripts and returns its index.
+
+Returns:
+  Added function's index.
+
+-----------------------------------------------------------
+ACSVM::Environment::addCodeDataACS0
+-----------------------------------------------------------
+
+Synopsis:
+  void addCodeDataACS0(Word code, CodeDataACS0 &&data);
+
+Description:
+  Adds a translation from instruction code for ACS0 and derived bytecode.
+
+-----------------------------------------------------------
+ACSVM::Environment::addFuncDataACS0
+-----------------------------------------------------------
+
+Synopsis:
+  void addFuncDataACS0(Word func, FuncDataACS0 &&data);
+
+Description:
+  Adds a translation from callfunc func for ACS0 and derived bytecode.
+
+-----------------------------------------------------------
+ACSVM::Environment::checkLock
+-----------------------------------------------------------
+
+Synopsis:
+  virtual bool checkLock(Thread *thread, Word lock, bool door);
+
+Description:
+  Called to check if a given lock number can be used from a given thread. The
+  lock number has no internal semantics, and is passed from the user source
+  unaltered.
+
+  The base implementation always return false.
+
+Returns:
+  True if the lock is open for that thread, false otherwise.
+
+-----------------------------------------------------------
+ACSVM::Environment::checkTag
+-----------------------------------------------------------
+
+Synopsis:
+  virtual bool checkTag(Word type, Word tag);
+
+Description:
+  Called to check if a given tag is inactive. The tag type and number both have
+  no internal semantics, and are passed from the user source unaltered.
+
+  The base implementation always returns false.
+
+Returns:
+  True if the tag is inactive, false otherwise.
+
+-----------------------------------------------------------
+ACSVM::Environment::collectStrings
+-----------------------------------------------------------
+
+Synopsis:
+  void collectStrings();
+
+Description:
+  Performs a full scan of the environment and frees strings that are no longer
+  in use.
+
+-----------------------------------------------------------
+ACSVM::Environment::exec
+-----------------------------------------------------------
+
+Synopsis:
+  virtual void exec();
+
+Description:
+  Performs a single execution cycle. Deferred script actions will be applied,
+  and active threads will execute until they terminate or enter a wait state.
+
+-----------------------------------------------------------
+ACSVM::Environment::freeGlobalScope
+-----------------------------------------------------------
+
+Synopsis:
+  void freeGlobalScope(GlobalScope *scope);
+
+Description:
+  Destructs and deallocates a contained GlobalScope object.
+
+-----------------------------------------------------------
+ACSVM::Environment::freeModule
+-----------------------------------------------------------
+
+Synopsis:
+  void freeModule(Module *module);
+
+Description:
+  Destructs and deallocates a contained Module object.
+
+  If any other modules reference the freed module, they must also be freed.
+
+-----------------------------------------------------------
+ACSVM::Environment::getGlobalScope
+-----------------------------------------------------------
+
+Synopsis:
+  GlobalScope *getGlobalScope(Word id);
+
+Description:
+  Retrieves a GlobalScope object by its identifier number. If it does not
+  exist, it will be created.
+
+Returns:
+  GlobalScope object with given id.
+
+-----------------------------------------------------------
+ACSVM::Environment::getModule
+-----------------------------------------------------------
+
+Synopsis:
+  Module *getModule(ModuleName const &name);
+
+Description:
+  Retrieves a Module object by name. If it does not exist or is not loaded, it
+  will be created and loaded as needed.
+
+Returns:
+  Module object with given name.
+
+-----------------------------------------------------------
+ACSVM::Environment::getModuleName
+-----------------------------------------------------------
+
+Synopsis:
+  ModuleName getModuleName(char const *str);
+  virtual ModuleName getModuleName(char const *str, std::size_t len);
+
+Description:
+  Generates a ModuleName from an input string. The first form calls the second,
+  using the null-terminated length of the input string.
+
+  The base implementation converts the input string into a String object for
+  ModuleName::s, leaving the other ModuleName fields set to 0.
+
+Returns:
+  ModuleName object formed from input string.
+
+-----------------------------------------------------------
+ACSVM::Environment::getScriptType
+-----------------------------------------------------------
+
+Synopsis:
+  virtual std::pair<Word, Word> getScriptTypeACS0(Word name);
+  virtual Word getScriptTypeACSE(Word type);
+
+Description:
+  Translates a bytecode script type into an internal type.
+
+  First form takes the script number and must return the type and name.
+
+  The base implementation of the first form translates by dividing by 1000. The
+  second form returns the type unaltered.
+
+Returns:
+  Translated script type or (type, name) pair.
+
+-----------------------------------------------------------
+ACSVM::Environment::getString
+-----------------------------------------------------------
+
+Synopsis:
+  String *getString(Word idx);
+  String *getString(char const *first, char const *last);
+  String *getString(char const *str);
+  String *getString(char const *str, std::size_t len);
+  String *getString(StringData const *data);
+
+Description:
+  First form returns a String object as if by calling (&stringTable[~idx]).
+
+  Second, third, and fourth forms create a StringData from input to find or
+  create an entry in stringTable.
+
+  Fifth form uses the supplied StringData object to find or create an entry in
+  stringTable. If data is null, null is returned. This is intended primarily
+  for resetting strings after deserialization.
+
+Returns:
+  First form returns the String object with the given index. All other forms
+  return a String object with the same data as the input.
+
+  Fifth form will return null if input is null, and non-null otherwise. All
+  other forms never return null.
+
+-----------------------------------------------------------
+ACSVM::Environment::hasActiveThread
+-----------------------------------------------------------
+
+Synopsis:
+  bool hasActiveThread() const;
+
+Description:
+  Checks for any active threads. A thread is considered active if it has any
+  state other than ThreadState::Inactive. So this will include threads that are
+  delayed, waiting for a condition (script or tag), or set to stop during the
+  next execution cycle.
+
+Returns:
+  True if there are any active threads, false otherwise.
+
+-----------------------------------------------------------
+ACSVM::Environment::loadState
+-----------------------------------------------------------
+
+Synopsis:
+  virtual void loadState(Serial &in);
+
+Description:
+  Restores the environment state from a previously serialized instance. If in
+  does not contain a byte stream generated by a previous call to saveState, the
+  behavior is undefined.
+
+-----------------------------------------------------------
+ACSVM::Environment::printArray
+-----------------------------------------------------------
+
+Synopsis:
+  virtual void printArray(PrintBuf &buf, Array const &array, Word index,
+    Word limit);
+
+Description:
+  Called to write a null-terminated character subsequence from an Array object
+  to a print buffer.
+
+  The base implementation calls PrintArrayChar.
+
+-----------------------------------------------------------
+ACSVM::Environment::printKill
+-----------------------------------------------------------
+
+Synopsis:
+  virtual void printKill(Thread *thread, Word type, Word data);
+
+Description:
+  Called when a thread encounters an error and must terminate. This includes
+  executing Code::Kill or calling Func::Kill. When calling by ACSVM itself, the
+  type parameter has a value from the KillType enumeration.
+
+  This function is expected to return normally, and the caller will handle
+  thread termination.
+
+  The base implementation prints kill information to std::cerr.
+
+-----------------------------------------------------------
+ACSVM::Environment::readModuleName
+-----------------------------------------------------------
+
+Synopsis:
+  virtual ModuleName readModuleName(Serial &in) const;
+
+Description:
+  Called to read a ModuleName from a serialized environment.
+
+  The base implementation reads the s and i members, leaving the p member null.
+
+Returns:
+  Deserialized ModuleName.
+
+-----------------------------------------------------------
+ACSVM::Environment::readString
+-----------------------------------------------------------
+
+Synopsis:
+  String *readString(Serial &in) const;
+
+Description:
+  Reads a String by index from a serialized Environment. If the written String
+  pointer was null, this function returns a null pointer.
+
+Returns:
+  Deserialized String.
+
+-----------------------------------------------------------
+ACSVM::Environment::refStrings
+-----------------------------------------------------------
+
+Synopsis:
+  virtual void refStrings();
+
+Description:
+  Called by collectStrings to mark contained strings as referenced.
+
+  The base implementation marks strings of all contained objects, as well as
+  performs an exhaustive scan of VM memory for string indexes.
+
+-----------------------------------------------------------
+ACSVM::Environment::resetStrings
+-----------------------------------------------------------
+
+Synopsis:
+  virtual void resetStrings();
+
+Description:
+  Called by loadState after reading the new StringTable to reset any String
+  pointers to the corresponding entry in the read table.
+
+  The base implementation resets strings of all contained objects.
+
+-----------------------------------------------------------
+ACSVM::Environment::saveState
+-----------------------------------------------------------
+
+Synopsis:
+  virtual void saveState(Serial &out) const;
+
+Description:
+  Serializes the environment state, which can be restored with a call to
+  loadState.
+
+-----------------------------------------------------------
+ACSVM::Environment::writeModuleName
+-----------------------------------------------------------
+
+Synopsis:
+  virtual void writeModuleName(Serial &out,
+    ModuleName const &name) const;
+
+Description:
+  Called to write a ModuleName.
+
+  The base implementation writes the s and i members.
+
+-----------------------------------------------------------
+ACSVM::Environment::writeString
+-----------------------------------------------------------
+
+Synopsis:
+  void writeString(Serial &out, String const *in) const;
+
+Description:
+  Writes a String by index. If in is null, a null pointer will be returned by
+  the corresponding call to readString.
+
+===============================================================================
+Errors <ACSVM/ACSVM/Error.hpp>
+===============================================================================
+
+===========================================================
+ACSVM::ReadError
+===========================================================
+
+Synopsis:
+  #include <ACSVM/ACSVM/Error.hpp>
+  class ReadError : public std::exception
+  {
+  public:
+    ReadError(char const *msg = "ACSVM::ReadError");
+
+    virtual char const *what() const noexcept;
+  };
+
+===============================================================================
+Modules <ACSVM/ACSVM/Module.hpp>
+===============================================================================
+
+===========================================================
+ACSVM::ModuleName
+===========================================================
+
+Synopsis:
+  #include <ACSVM/ACSVM/Module.hpp>
+  class ModuleName
+  {
+  public:
+    ModuleName(String *s, void *p, std::size_t i);
+
+    bool operator == (ModuleName const &name) const;
+    bool operator != (ModuleName const &name) const;
+
+    std::size_t hash() const;
+
+    String     *s;
+    void       *p;
+    std::size_t i;
+  };
+
+===========================================================
+ACSVM::Module
+===========================================================
+
+Synopsis:
+  #include <ACSVM/ACSVM/Module.hpp>
+  class Module
+  {
+  public:
+    void readBytecode(Byte const *data, std::size_t size);
+
+    Environment *env;
+    ModuleName   name;
+
+    bool isACS0;
+    bool loaded;
+
+
+    static constexpr std::uint32_t ChunkID(char c0, char c1, char c2, char c3);
+
+    static constexpr std::uint32_t ChunkID(char const (&s)[5]);
+  };
+
+===============================================================================
+Print Buffers <ACSVM/ACSVM/PrintBuf.hpp>
+===============================================================================
+
+===========================================================
+ACSVM::PrintBuf
+===========================================================
+
+Synopsis:
+  #include <ACSVM/ACSVM/PrintBuf.hpp>
+  class PrintBuf
+  {
+  public:
+    PrintBuf();
+    ~PrintBuf();
+
+    void clear();
+
+    char const *data() const;
+    char const *dataFull() const;
+
+    void drop();
+
+    void format(char const *fmt, ...);
+    void formatv(char const *fmt, std::va_list arg);
+
+    char *getBuf(std::size_t count);
+
+    char *getLoadBuf(std::size_t countFull, std::size_t count);
+
+    void push();
+
+    void put(char c);
+    void put(char const *s);
+    void put(char const *s, std::size_t n);
+
+    void reserve(std::size_t count);
+
+    std::size_t size() const;
+    std::size_t sizeFull() const;
+  };
+
+===============================================================================
+Scopes <ACSVM/ACSVM/Scope.hpp>
+===============================================================================
+
+===========================================================
+ACSVM::GlobalScope
+===========================================================
+
+Synopsis:
+  #include <ACSVM/ACSVM/Scope.hpp>
+  class GlobalScope
+  {
+  public:
+    static constexpr std::size_t ArrC = 256;
+    static constexpr std::size_t RegC = 256;
+
+
+    void freeHubScope(HubScope *scope);
+
+    HubScope *getHubScope(Word id);
+
+    bool hasActiveThread() const;
+
+    void lockStrings() const;
+
+    void reset();
+
+    void unlockStrings() const;
+
+    Environment *const env;
+    Word         const id;
+
+    Array arrV[ArrC];
+    Word  regV[RegC];
+
+    bool active;
+  };
+
+===========================================================
+ACSVM::HubScope
+===========================================================
+
+Synopsis:
+  #include <ACSVM/ACSVM/Scope.hpp>
+  class HubScope
+  {
+  public:
+    static constexpr std::size_t ArrC = 256;
+    static constexpr std::size_t RegC = 256;
+
+
+    void freeMapScope(MapScope *scope);
+
+    MapScope *getMapScope(Word id);
+
+    bool hasActiveThread() const;
+
+    void lockStrings() const;
+
+    void reset();
+
+    void unlockStrings() const;
+
+    Environment *const env;
+    GlobalScope *const global;
+    Word         const id;
+
+    Array arrV[ArrC];
+    Word  regV[RegC];
+
+    bool active;
+  };
+
+===========================================================
+ACSVM::MapScope
+===========================================================
+
+Synopsis:
+  #include <ACSVM/ACSVM/Scope.hpp>
+  class MapScope
+  {
+  public:
+    using ScriptStartFunc = void (*)(Thread *);
+    using ScriptStartFuncC = MapScope_ScriptStartFuncC;
+
+    class ScriptStartInfo;
+
+
+    void addModules(Module *const *moduleV, std::size_t moduleC);
+
+    Script *findScript(ScriptName name);
+
+    ModuleScope *getModuleScope(Module *module);
+
+    String *getString(Word idx) const;
+
+    bool hasActiveThread() const;
+
+    bool hasModules() const;
+
+    bool isScriptActive(Script *script);
+
+   void loadState(Serial &in);
+
+    void lockStrings() const;
+
+    void reset();
+
+    void saveState(Serial &out) const;
+
+    bool scriptPause(Script *script);
+    bool scriptPause(ScriptName name, ScopeID scope);
+    bool scriptStart(Script *script, ScriptStartInfo const &info);
+    bool scriptStart(ScriptName name, ScopeID scope,
+      ScriptStartInfo const &info);
+    bool scriptStartForced(Script *script, ScriptStartInfo const &info);
+    bool scriptStartForced(ScriptName name, ScopeID scope,
+      ScriptStartInfo const &info);
+    Word scriptStartResult(Script *script, ScriptStartInfo const &info);
+    Word scriptStartResult(ScriptName name, ScriptStartInfo const &info);
+    Word scriptStartType(Word type, ScriptStartInfo const &info);
+    Word scriptStartTypeForced(Word type, ScriptStartInfo const &info);
+    bool scriptStop(Script *script);
+    bool scriptStop(ScriptName name, ScopeID scope);
+
+    void unlockStrings() const;
+
+    Environment *const env;
+    HubScope    *const hub;
+    Word         const id;
+
+    bool active;
+    bool clampCallSpec;
+  };
+
+===========================================================
+ACSVM::MapScope::ScriptStartInfo
+===========================================================
+
+Synopsis:
+  #include <ACSVM/ACSVM/Scope.hpp>
+  class ScriptStartInfo
+  {
+  public:
+    ScriptStartInfo();
+    ScriptStartInfo(Word const *argV, std::size_t argC,
+       ThreadInfo const *info = nullptr, ScriptStartFunc func = nullptr);
+    ScriptStartInfo(Word const *argV, std::size_t argC,
+       ThreadInfo const *info, ScriptStartFuncC func);
+
+    Word       const *argV;
+    ScriptStartFunc   func;
+    ScriptStartFuncC  funcc;
+    ThreadInfo const *info;
+    std::size_t       argC;
+  };
+
+===========================================================
+ACSVM::ModuleScope
+===========================================================
+
+Synopsis:
+  #include <ACSVM/ACSVM/Scope.hpp>
+  class ModuleScope
+  {
+  public:
+    static constexpr std::size_t ArrC = 256;
+    static constexpr std::size_t RegC = 256;
+
+
+    void lockStrings() const;
+
+    void unlockStrings() const;
+
+    Environment *const env;
+    MapScope    *const map;
+    Module      *const module;
+
+    Array *arrV[ArrC];
+    Word  *regV[RegC];
+  };
+
+===============================================================================
+Scripts <ACSVM/ACSVM/Script.hpp>
+===============================================================================
+
+===========================================================
+ACSVM::ScriptName
+===========================================================
+
+Synopsis:
+  #include <ACSVM/ACSVM/Script.hpp>
+  class ScriptName
+  {
+  public:
+    ScriptName();
+    ScriptName(String *s);
+    ScriptName(String *s, Word i);
+    ScriptName(Word i);
+
+    String *s;
+    Word    i;
+  };
+
+===========================================================
+ACSVM::Script
+===========================================================
+
+Synopsis:
+  #include <ACSVM/ACSVM/Script.hpp>
+  class Script
+  {
+  public:
+    explicit Script(Module *module);
+    ~Script();
+
+    Module *const module;
+
+    ScriptName name;
+
+    Word argC;
+    Word codeIdx;
+    Word flags;
+    Word locArrC;
+    Word locRegC;
+    Word type;
+
+    bool flagClient : 1;
+    bool flagNet    : 1;
+  };
+
+===============================================================================
+Stacks <ACSVM/ACSVM/Stack.hpp>
+===============================================================================
+
+===========================================================
+ACSVM::Stack
+===========================================================
+
+Synopsis:
+  #include <ACSVM/ACSVM/Stack.hpp>
+  template<typename T>
+  class Stack
+  {
+  public:
+    Stack();
+    ~Stack();
+
+    T &operator [] (std::size_t idx);
+
+    T       *begin();
+    T const *begin() const;
+
+    void clear();
+
+    void drop();
+    void drop(std::size_t n);
+
+    bool empty() const;
+
+    T       *end();
+    T const *end() const;
+
+    void push(T const &value);
+    void push(T      &&value);
+
+    void reserve(std::size_t count);
+
+    std::size_t size() const;
+  };
+
+===============================================================================
+Locals Storage <ACSVM/ACSVM/Store.hpp>
+===============================================================================
+
+===========================================================
+ACSVM::Store
+===========================================================
+
+Synopsis:
+  #include <ACSVM/ACSVM/Store.hpp>
+  template<typename T>
+  class Store
+  {
+  public:
+    Store();
+    ~Store();
+
+    T &operator [] (std::size_t idx);
+
+    void alloc(std::size_t count);
+
+    void allocLoad(std::size_t countFull, std::size_t count);
+
+    T       *begin();
+    T const *begin() const;
+
+    T       *beginFull();
+    T const *beginFull() const;
+
+    void clear();
+
+    T const *dataFull() const;
+
+    T       *end();
+    T const *end() const;
+
+    void free(std::size_t count);
+
+    std::size_t size() const;
+
+    std::size_t sizeFull() const;
+  };
+
+===============================================================================
+Strings <ACSVM/ACSVM/String.hpp>
+===============================================================================
+
+===========================================================
+ACSVM::StringData
+===========================================================
+
+Synopsis:
+  #include <ACSVM/ACSVM/String.hpp>
+  class StringData
+  {
+  public:
+    StringData(char const *first, char const *last);
+    StringData(char const *str, std::size_t len);
+    StringData(char const *str, std::size_t len, std::size_t hash);
+
+    bool operator == (StringData const &r) const;
+
+    char const *const str;
+    std::size_t const len;
+    std::size_t const hash;
+  };
+
+===========================================================
+ACSVM::String
+===========================================================
+
+Synopsis:
+  #include <ACSVM/ACSVM/String.hpp>
+  class String : public StringData
+  {
+  public:
+    std::size_t lock;
+
+    Word const idx;
+    Word const len0;
+
+    bool ref;
+
+    char get(std::size_t i) const;
+  };
+
+===========================================================
+ACSVM::StringTable
+===========================================================
+
+Synopsis:
+  #include <ACSVM/ACSVM/String.hpp>
+  class StringTable
+  {
+  public:
+    StringTable();
+    StringTable(StringTable &&table);
+    ~StringTable();
+
+    String &operator [] (Word idx) const;
+    String &operator [] (StringData const &data);
+
+    void clear();
+
+    void collectBegin();
+    void collectEnd();
+
+    String &getNone();
+
+    std::size_t size() const;
+  };
+
+===============================================================================
+Threads <ACSVM/ACSVM/Thread.hpp>
+===============================================================================
+
+===========================================================
+ACSVM::ThreadState
+===========================================================
+
+Synopsis:
+  #include <ACSVM/ACSVM/Thread.hpp>
+  class ThreadState
+  {
+  public:
+    enum State
+    {
+      Inactive,
+      Running,
+      Stopped,
+      Paused,
+      WaitScrI,
+      WaitScrS,
+      WaitTag,
+    };
+
+
+    ThreadState();
+    ThreadState(State state);
+    ThreadState(State state, Word data);
+    ThreadState(State state, Word data, Word type);
+
+    bool operator == (State s) const;
+    bool operator != (State s) const;
+
+    State state;
+    Word data;
+    Word type;
+  };
+
+===========================================================
+ACSVM::ThreadInfo
+===========================================================
+
+Synopsis:
+  #include <ACSVM/ACSVM/Thread.hpp>
+  class ThreadInfo
+  {
+  public:
+    virtual ~ThreadInfo() {}
+  };
+
+===========================================================
+ACSVM::Thread
+===========================================================
+
+Synopsis:
+  #include <ACSVM/ACSVM/Thread.hpp>
+  class Thread
+  {
+  public:
+    virtual ThreadInfo const *getInfo() const;
+
+    virtual void lockStrings() const;
+
+    virtual void unlockStrings() const;
+
+    Environment *const env;
+
+    Stack<CallFrame> callStk;
+    Stack<Word>      dataStk;
+    Store<Array>     localArr;
+    Store<Word>      localReg;
+    PrintBuf         printBuf;
+    ThreadState      state;
+
+    Word  const *codePtr;
+    Module      *module;
+    GlobalScope *scopeGbl;
+    HubScope    *scopeHub;
+    MapScope    *scopeMap;
+    ModuleScope *scopeMod;
+    Script      *script;
+    Word         delay;
+    Word         result;
+  };
+
+###############################################################################
+EOF
+###############################################################################
+
diff --git a/src/b_bot.c b/src/b_bot.c
index af57d65ecf6e3c24e725ed38b9533b9ab6509763..483a9d0650c268569e098eb9440ccc0118732755 100644
--- a/src/b_bot.c
+++ b/src/b_bot.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 2007-2016 by John "JTE" Muniz.
-// Copyright (C) 2011-2023 by Sonic Team Junior.
+// Copyright (C) 2011-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -14,11 +14,89 @@
 #include "d_player.h"
 #include "g_game.h"
 #include "r_main.h"
+#include "r_skins.h"
+#include "hu_stuff.h"
 #include "p_local.h"
 #include "b_bot.h"
 #include "lua_hook.h"
 #include "i_system.h" // I_BaseTiccmd
 
+INT16 B_AddBot(const char *skinname, UINT16 skincolor, const char *name, SINT8 type)
+{
+	INT16 i, newplayernum;
+	player_t *newplayer;
+	SINT8 skinnum = 0;
+
+	for (i = 0; i < MAXPLAYERS; i++)
+	{
+		if (!playeringame[i])
+			break;
+	}
+
+	if (i >= MAXPLAYERS)
+	{
+		return -1;
+	}
+
+	newplayernum = i;
+
+	CL_ClearPlayer(newplayernum);
+
+	playeringame[newplayernum] = true;
+	G_AddPlayer(newplayernum);
+	newplayer = &players[newplayernum];
+
+	newplayer->jointime = 0;
+	newplayer->quittime = 0;
+	newplayer->lastinputtime = 0;
+
+	// Set the skin (defaults to Sonic)
+	if (skinname)
+	{
+		skinnum = R_SkinAvailable(skinname);
+		skinnum = skinnum < 0 ? 0 : skinnum;
+	}
+
+	// Set the color (defaults to skin prefcolor)
+	if (skincolor == SKINCOLOR_NONE)
+		newplayer->skincolor = skins[skinnum]->prefcolor;
+	else
+		newplayer->skincolor = skincolor;
+
+	// Set the bot default name as the skin
+	strcpy(player_names[newplayernum], skins[skinnum]->realname);
+
+	// Read the bot name, if given
+	if (name != NULL)
+		strlcpy(player_names[newplayernum], name, sizeof(*player_names));
+
+	newplayer->bot = (type >= BOT_NONE && type <= BOT_MPAI) ? type : BOT_MPAI;
+
+	// If our bot is a 2P type, we'll need to set its leader so it can spawn
+	if (newplayer->bot == BOT_2PAI || newplayer->bot == BOT_2PHUMAN)
+		B_UpdateBotleader(newplayer);
+
+	// Set the skin (can't do this until AFTER bot type is set!)
+	SetPlayerSkinByNum(newplayernum, skinnum);
+
+	if (netgame)
+	{
+		char joinmsg[256];
+
+		// Truncate bot name
+		player_names[newplayernum][sizeof(*player_names) - 8] = '\0'; // The length of colored [BOT] + 1
+
+		strcpy(joinmsg, M_GetText("\x82*Bot %s has joined the game (player %d)"));
+		strcpy(joinmsg, va(joinmsg, player_names[newplayernum], newplayernum));
+		HU_AddChatText(joinmsg, false);
+
+		// Append blue [BOT] tag at the end
+		strlcat(player_names[newplayernum], "\x84[BOT]\x80", sizeof(*player_names));
+	}
+
+	return newplayernum;
+}
+
 void B_UpdateBotleader(player_t *player)
 {
 	UINT32 i;
diff --git a/src/b_bot.h b/src/b_bot.h
index bbe0829bea7274297dd3718d553aec9d4c23a9d6..1115e65eb65dd9b3217662866ea9c98563aca8b4 100644
--- a/src/b_bot.h
+++ b/src/b_bot.h
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 2007-2016 by John "JTE" Muniz.
-// Copyright (C) 2012-2023 by Sonic Team Junior.
+// Copyright (C) 2012-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -10,6 +10,14 @@
 /// \file  b_bot.h
 /// \brief Basic bot handling
 
+#ifndef __B_BOT__
+#define __B_BOT__
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+INT16 B_AddBot(const char *skinname, UINT16 skincolor, const char *name, SINT8 type);
 void B_UpdateBotleader(player_t *player);
 void B_BuildTiccmd(player_t *player, ticcmd_t *cmd);
 void B_KeysToTiccmd(mobj_t *mo, ticcmd_t *cmd, boolean forward, boolean backward, boolean left, boolean right, boolean strafeleft, boolean straferight, boolean jump, boolean spin);
@@ -17,3 +25,9 @@ boolean B_CheckRespawn(player_t *player);
 void B_MoveBlocked(player_t *player);
 void B_RespawnBot(INT32 playernum);
 void B_HandleFlightIndicator(player_t *player);
+
+#ifdef __cplusplus
+} // extern "C"
+#endif
+
+#endif
diff --git a/src/cxxutil.hpp b/src/cxxutil.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..d7405191419d50154de103c0ea1590608d91cb9a
--- /dev/null
+++ b/src/cxxutil.hpp
@@ -0,0 +1,194 @@
+// SONIC ROBO BLAST 2
+//-----------------------------------------------------------------------------
+// Copyright (C) 2024 by Ronald "Eidolon" Kinard
+// Copyright (C) 2024 by Kart Krew
+//
+// This program is free software distributed under the
+// terms of the GNU General Public License, version 2.
+// See the 'LICENSE' file for more details.
+//-----------------------------------------------------------------------------
+
+#ifndef __SRB2_CXXUTIL_HPP__
+#define __SRB2_CXXUTIL_HPP__
+
+#include <cstdlib>
+#include <functional>
+#include <type_traits>
+#include <utility>
+
+#include "doomdef.h"
+
+namespace srb2 {
+
+template <class F>
+class Finally {
+public:
+	explicit Finally(const F& f) noexcept : f_(f) {}
+	explicit Finally(F&& f) noexcept : f_(f) {}
+
+	Finally(Finally&& from) noexcept : f_(std::move(from.f_)), call_(std::exchange(from.call_, false)) {}
+
+	Finally(const Finally& from) = delete;
+	void operator=(const Finally& from) = delete;
+	void operator=(Finally&& from) = delete;
+
+	~Finally() noexcept {
+		if (call_)
+			f_();
+	}
+
+private:
+	F f_;
+	bool call_ = true;
+};
+
+template <class F>
+Finally<std::decay_t<F>> finally(F&& f) noexcept {
+	return Finally {std::forward<F>(f)};
+}
+
+struct SourceLocation {
+	const char* file_name;
+	unsigned int line_number;
+};
+
+#define SRB2_SOURCE_LOCATION                                                                                           \
+	srb2::SourceLocation { __FILE__, __LINE__ }
+
+#ifndef SRB2_ASSERT_HANDLER
+#define SRB2_ASSERT_HANDLER srb2::IErrorAssertHandler
+#endif
+
+#if !defined(NDEBUG) || defined(PARANOIA)
+// An assertion level of 2 will activate all invocations of the SRB2_ASSERT macro
+#define SRB2_ASSERTION_LEVEL 2
+#else
+// The minimum assertion level is 1
+#define SRB2_ASSERTION_LEVEL 1
+#endif
+
+/// Assert a precondition expression in debug builds.
+#define SRB2_ASSERT(expr) srb2::do_assert<2, SRB2_ASSERT_HANDLER>([&] { return (expr); }, SRB2_SOURCE_LOCATION, #expr)
+
+class IErrorAssertHandler {
+public:
+	static void handle(const SourceLocation& source_location, const char* expression) {
+		I_Error(
+			"Assertion failed at %s:%u: %s != true",
+			source_location.file_name,
+			source_location.line_number,
+			expression
+		);
+	}
+};
+
+class NoOpAssertHandler {
+public:
+	static void handle(const SourceLocation& source_location, const char* expression) {
+		(void)source_location;
+		(void)expression;
+	}
+};
+
+/// @brief Assert a precondition expression, aborting the application if it fails.
+/// @tparam Expr
+/// @tparam Level the level of this assertion; if it is less than or equal to SRB2_ASSERTION_LEVEL, this overload will
+/// activate.
+/// @param expr a callable which returns a bool
+/// @param source_location a struct containing the source location of the assertion, e.g. SRB2_SOURCE_LOCATION
+/// @param expression the expression evaluated in the expression callable
+/// @param message an optional message to display for the assertion
+template <unsigned int Level, class Handler, class Expr>
+std::enable_if_t<(Level <= SRB2_ASSERTION_LEVEL), void>
+do_assert(const Expr& expr, const SourceLocation& source_location, const char* expression = "") noexcept {
+	static_assert(Level > 0, "level of an assertion must not be 0");
+	if (!expr()) {
+		Handler::handle(source_location, expression);
+		std::abort();
+	}
+}
+
+template <unsigned int Level, class, class Expr>
+std::enable_if_t<(Level > SRB2_ASSERTION_LEVEL), void>
+do_assert(const Expr&, const SourceLocation&, const char* = "") noexcept {
+}
+
+template <typename T>
+class NotNull final {
+	T ptr_;
+
+public:
+	static_assert(
+		std::is_convertible_v<decltype(std::declval<T>() != nullptr), bool>,
+		"T is not comparable with nullptr_t"
+	);
+
+	/// @brief Move-construct from the pointer value U, asserting that it is not null. Allows construction of a
+	/// NotNull<T> from any compatible pointer U, for example with polymorphic classes.
+	template <typename U, typename = std::enable_if_t<std::is_convertible_v<U, T>>>
+	constexpr NotNull(U&& rhs) : ptr_(std::forward<U>(rhs)) {
+		SRB2_ASSERT(ptr_ != nullptr);
+	}
+
+	/// @brief Wrap the pointer type T, asserting that the pointer is not null.
+	template <typename = std::enable_if_t<!std::is_same_v<std::nullptr_t, T>>>
+	constexpr NotNull(T rhs) : ptr_(std::move(rhs)) {
+		SRB2_ASSERT(ptr_ != nullptr);
+	}
+
+	/// @brief Copy construction from NotNull of convertible type U. Only if the incoming pointer is NotNull already.
+	template <typename U, typename = std::enable_if_t<std::is_convertible_v<U, T>>>
+	constexpr NotNull(const NotNull<U>& rhs) : NotNull(rhs.get()) {
+		// Value is guaranteed to be not null by construction; no assertion necessary
+	}
+
+	NotNull(const NotNull& rhs) = default;
+	NotNull& operator=(const NotNull& rhs) = default;
+
+	/// @brief Get the stored pointer.
+	constexpr T get() const { return ptr_; }
+
+	/// @brief Convert to T (the pointer type).
+	constexpr operator T() const { return get(); }
+
+	/// @brief Arrow-dereference to *T (the actual value pointed to).
+	constexpr decltype(auto) operator->() const { return get(); }
+
+	/// @brief Dereference to *T (the actual value pointed to).
+	constexpr decltype(auto) operator*() const { return *get(); }
+
+	// It is not allowed to construct NotNull<T> with nullptr regardless of T.
+	NotNull(std::nullptr_t) = delete;
+	NotNull& operator=(std::nullptr_t) = delete;
+};
+
+template <class T>
+NotNull(T) -> NotNull<T>;
+
+/// @brief Utility struct for combining several Callables (e.g. lambdas) into a single Callable with the call operator
+/// overloaded. Use it to build a visitor for calling std::visit on variants.
+/// @tparam ...Ts callable types
+template <typename... Ts>
+struct Overload : Ts... {
+	using Ts::operator()...;
+};
+
+template <typename... Ts>
+Overload(Ts...) -> Overload<Ts...>;
+
+inline void hash_combine(std::size_t& seed)
+{
+	(void)seed;
+}
+
+template <class T, typename... Rest>
+inline void hash_combine(std::size_t& seed, const T& v, Rest... rest)
+{
+	std::hash<T> hasher;
+	seed ^= hasher(v) + 0x9e3779b9 + (seed << 6) + (seed >> 2);
+	hash_combine(seed, std::forward<Rest>(rest)...);
+}
+
+} // namespace srb2
+
+#endif // __SRB2_CXXUTIL_HPP__
diff --git a/src/d_main.c b/src/d_main.c
index c139650d1eb039a057da10d063f392f33c94fac2..082df31cd5841292ad67edea07fe88bceeee051b 100644
--- a/src/d_main.c
+++ b/src/d_main.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -94,6 +94,8 @@
 
 #include "lua_script.h"
 
+#include "acs/interface.h"
+
 // Version numbers for netplay :upside_down_face:
 int    VERSION;
 int SUBVERSION;
@@ -1596,6 +1598,9 @@ void D_SRB2Main(void)
 	CONS_Printf("ST_Init(): Init status bar.\n");
 	ST_Init();
 
+	CONS_Printf("ACS_Init(): Init Action Code Script VM.\n");
+	ACS_Init();
+
 	if (M_CheckParm("-room"))
 	{
 		if (!M_IsNextParm())
diff --git a/src/d_main.h b/src/d_main.h
index 0a29f929b178dc6ca592fe59a278564418c3825c..b4615fb3c55b144d7debf6603b1e3abf0aac1b02 100644
--- a/src/d_main.h
+++ b/src/d_main.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/d_player.h b/src/d_player.h
index 95d2f609dab322189fcb3892d7beace6d0fd45dc..a2f783cf20e1471307842c96bfdb247391a8a53b 100644
--- a/src/d_player.h
+++ b/src/d_player.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -614,6 +614,7 @@ typedef struct player_s
 	boolean spectator;
 	boolean outofcoop;
 	boolean removing;
+	boolean enteredgame;
 	boolean muted;
 	UINT8 bot;
 	struct player_s *botleader;
diff --git a/src/deh_lua.c b/src/deh_lua.c
index 64fb52fc7423a7486c9d6736762fedd0fcde64ba..8aa5ba358870899aa5dcfc98d7740200ee766882 100644
--- a/src/deh_lua.c
+++ b/src/deh_lua.c
@@ -414,6 +414,26 @@ static int ScanConstants(lua_State *L, boolean mathlib, const char *word)
 		if (mathlib) return luaL_error(L, "sector triggerer '%s' could not be found.\n", word);
 		return 0;
 	}
+	else if (fastncmp("SPAC_", word, 3)) {
+		p = word + 5;
+		for (i = 0; SPAC_LIST[i]; i++)
+			if (fastcmp(p, SPAC_LIST[i])) {
+				lua_pushinteger(L, i);
+				return 1;
+			}
+		if (mathlib) return luaL_error(L, "line activation '%s' could not be found.\n", word);
+		return 0;
+	}
+	else if (fastncmp("SECSPAC_", word, 3)) {
+		p = word + 8;
+		for (i = 0; SECSPAC_LIST[i]; i++)
+			if (fastcmp(p, SECSPAC_LIST[i])) {
+				lua_pushinteger(L, i);
+				return 1;
+			}
+		if (mathlib) return luaL_error(L, "sector activation '%s' could not be found.\n", word);
+		return 0;
+	}
 	else if (fastncmp("S_",word,2)) {
 		p = word+2;
 		for (i = 0; i < NUMSTATEFREESLOTS; i++) {
diff --git a/src/deh_tables.c b/src/deh_tables.c
index c7c7c604068cd4761ea3944a92cd4efecd9a0ddb..f807e574c7e415cd7bd30e4c82cdceff5bc84f43 100644
--- a/src/deh_tables.c
+++ b/src/deh_tables.c
@@ -4607,6 +4607,35 @@ const char *const TO_LIST[] = {
 	NULL
 };
 
+// Line activation flags
+const char *const SPAC_LIST[] = {
+	"REPEATSPECIAL",
+	"CROSS",
+	"CROSSMONSTER",
+	"CROSSMISSILE",
+	"PUSH",
+	"PUSHMONSTER",
+	"IMPACT",
+	NULL
+};
+
+// Sector activation flags
+const char *const SECSPAC_LIST[] = {
+	"ONCESPECIAL",
+	"REPEATSPECIAL",
+	"CONTINUOUSSPECIAL",
+	"ENTER",
+	"FLOOR",
+	"CEILING",
+	"ENTERMONSTER",
+	"FLOORMONSTER",
+	"CEILINGMONSTER",
+	"ENTERMISSILE",
+	"FLOORMISSILE",
+	"CEILINGMISSILE",
+	NULL
+};
+
 const char *COLOR_ENUMS[] = {
 	"NONE",			// SKINCOLOR_NONE,
 
diff --git a/src/deh_tables.h b/src/deh_tables.h
index b6986adff0166c3132be5f70ca78c7835030998f..6fd89a6950611b6a3b56596ba57ae099e127947f 100644
--- a/src/deh_tables.h
+++ b/src/deh_tables.h
@@ -70,6 +70,8 @@ extern const char *const MSF_LIST[]; // Sector flags
 extern const char *const SSF_LIST[]; // Sector special flags
 extern const char *const SD_LIST[]; // Sector damagetype
 extern const char *const TO_LIST[]; // Sector triggerer
+extern const char *const SPAC_LIST[]; // Line activation flags
+extern const char *const SECSPAC_LIST[]; // Sector activation flags
 extern const char *COLOR_ENUMS[];
 extern const char *const POWERS_LIST[];
 extern const char *const HUDITEMS_LIST[];
diff --git a/src/doomdata.h b/src/doomdata.h
index 276e03297b6f0d453ad0d0c65fc8bd9196d9680c..785242671badab31b99ba7e65e46a3eb8b2a45d5 100644
--- a/src/doomdata.h
+++ b/src/doomdata.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -26,6 +26,11 @@
 #include "taglist.h"
 #include "m_fixed.h" // See the mapthing_t scale.
 
+// Number of args for ACS scripts.
+// Increasing this requires you to also update the ACS compiler.
+#define NUM_SCRIPT_ARGS 10
+#define NUM_SCRIPT_STRINGARGS 2
+
 //
 // Map level types.
 // The following data structures define the persistent format
@@ -146,6 +151,30 @@ typedef struct
 
 #define ML_TFERLINE         32768
 
+enum
+{
+	// Special action is repeatable.
+	SPAC_REPEATSPECIAL = 0x00000001,
+
+	// Activates when crossed by a player.
+	SPAC_CROSS         = 0x00000002,
+
+	// Activates when crossed by an enemy.
+	SPAC_CROSSMONSTER  = 0x00000004,
+
+	// Activates when crossed by a projectile.
+	SPAC_CROSSMISSILE  = 0x00000008,
+
+	// Activates when bumped by a player.
+	SPAC_PUSH          = 0x00000010,
+
+	// Activates when bumped by an enemy.
+	SPAC_PUSHMONSTER   = 0x00000020,
+
+	// Activates when bumped by a missile.
+	SPAC_IMPACT        = 0x00000040,
+};
+
 // Sector definition, from editing.
 typedef struct
 {
@@ -200,8 +229,10 @@ typedef struct
 #pragma pack()
 #endif
 
-#define NUMMAPTHINGARGS 10
-#define NUMMAPTHINGSTRINGARGS 2
+// Number of args for thing behaviors.
+// These are safe to increase at any time.
+#define NUM_MAPTHING_ARGS 10
+#define NUM_MAPTHING_STRINGARGS 2
 
 // Thing definition, position, orientation and type,
 // plus visibility flags and attributes.
@@ -213,11 +244,15 @@ typedef struct
 	UINT16 options;
 	INT16 z;
 	UINT8 extrainfo;
+	mtag_t tid;
 	taglist_t tags;
 	fixed_t scale;
 	fixed_t spritexscale, spriteyscale;
-	INT32 args[NUMMAPTHINGARGS];
-	char *stringargs[NUMMAPTHINGSTRINGARGS];
+	INT32 args[NUM_MAPTHING_ARGS];
+	char *stringargs[NUM_MAPTHING_STRINGARGS];
+	INT16 special;
+	INT32 script_args[NUM_SCRIPT_ARGS];
+	char *script_stringargs[NUM_SCRIPT_STRINGARGS];
 	struct mobj_s *mobj;
 } mapthing_t;
 
diff --git a/src/doomdef.h b/src/doomdef.h
index 1b0e76314b13d080fc18c8f3463c066616c4b8c8..7fdd46fbe53ff79a0af4064d89d52edb5726a29e 100644
--- a/src/doomdef.h
+++ b/src/doomdef.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -106,6 +106,10 @@
 
 FILE *fopenfile(const char*, const char*);
 
+#ifdef __cplusplus
+extern "C" {
+#endif
+
 //#define NOMD5
 
 // If you don't disable ALL debug first, you get ALL debug enabled
@@ -151,10 +155,10 @@ extern char logfilename[1024];
 // VERSIONSTRING_RC is for the resource-definition script used by windows builds
 #else
 #ifdef BETAVERSION
-#define VERSIONSTRING "v"SRB2VERSION" "BETAVERSION
+#define VERSIONSTRING "v" SRB2VERSION " " BETAVERSION
 #define VERSIONSTRING_RC SRB2VERSION " " BETAVERSION "\0"
 #else
-#define VERSIONSTRING "v"SRB2VERSION
+#define VERSIONSTRING "v" SRB2VERSION
 #define VERSIONSTRING_RC SRB2VERSION "\0"
 #endif
 // Hey! If you change this, add 1 to the MODVERSION below!
@@ -236,6 +240,8 @@ extern char logfilename[1024];
 #define GETEXECVERSION(major,minor) (major + (minor << 16))
 #define EXECVERSION GETEXECVERSION(MAJOREXECVERSION, MINOREXECVERSION)
 
+#define UDMF_CURRENT_VERSION 0
+
 // =========================================================================
 
 // The maximum number of players, multiplayer/networking.
@@ -620,12 +626,14 @@ UINT32 quickncasehash (const char *p, size_t n)
 	return x;
 }
 
+#ifndef __cplusplus
 #ifndef min // Double-Check with WATTCP-32's cdefs.h
 #define min(x, y) (((x) < (y)) ? (x) : (y))
 #endif
 #ifndef max // Double-Check with WATTCP-32's cdefs.h
 #define max(x, y) (((x) > (y)) ? (x) : (y))
 #endif
+#endif
 
 // Max gamepad/joysticks that can be detected/used.
 #define MAX_JOYSTICKS 4
@@ -737,4 +745,8 @@ extern int
 #undef UPDATE_ALERT
 #endif
 
+#ifdef __cplusplus
+} // extern "C"
+#endif
+
 #endif // __DOOMDEF__
diff --git a/src/fastcmp.h b/src/fastcmp.h
index 0f7c24aaaa20ab3e05397c36e998f71415f43b9c..3500701f70aafbdbc83ed8093cda7907ebccc920 100644
--- a/src/fastcmp.h
+++ b/src/fastcmp.h
@@ -24,4 +24,11 @@ FUNCINLINE static ATTRINLINE boolean fastncmp(const char *s, const char *c, UINT
 	return !l; // make sure you reached the end
 }
 
+// case-insensitive of the above
+FUNCINLINE static ATTRINLINE boolean fastnicmp(const char *s, const char *c, UINT16 l)
+{
+	for (; *s && toupper(*s) == toupper(*c) && --l; s++, c++) ;
+	return !l; // make sure you reached the end
+}
+
 #endif
diff --git a/src/g_game.c b/src/g_game.c
index 8d19c9e7cb68df142e68e7d0277b2425153f9206..7d1a47c22865e3173d58c839c45c14da7e848afa 100644
--- a/src/g_game.c
+++ b/src/g_game.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -53,6 +53,8 @@
 #include "lua_hud.h"
 #include "lua_libs.h"
 
+#include "acs/interface.h"
+
 gameaction_t gameaction;
 gamestate_t gamestate = GS_NULL;
 UINT8 ultimatemode = false;
@@ -2614,6 +2616,7 @@ void G_PlayerReborn(INT32 player, boolean betweenmaps)
 	boolean spectator;
 	boolean outofcoop;
 	boolean removing;
+	boolean enteredgame;
 	INT16 bot;
 	SINT8 pity;
 	INT16 rings;
@@ -2630,6 +2633,7 @@ void G_PlayerReborn(INT32 player, boolean betweenmaps)
 	quittime = players[player].quittime;
 	spectator = players[player].spectator;
 	outofcoop = players[player].outofcoop;
+	enteredgame = players[player].enteredgame;
 	removing = players[player].removing;
 	pflags = (players[player].pflags & (PF_FLIPCAM|PF_ANALOGMODE|PF_DIRECTIONCHAR|PF_AUTOBRAKE|PF_TAGIT|PF_GAMETYPEOVER));
 	playerangleturn = players[player].angleturn;
@@ -2708,6 +2712,7 @@ void G_PlayerReborn(INT32 player, boolean betweenmaps)
 	p->spectator = spectator;
 	p->outofcoop = outofcoop;
 	p->removing = removing;
+	p->enteredgame = enteredgame;
 	p->angleturn = playerangleturn;
 	p->oldrelangleturn = oldrelangleturn;
 
@@ -2801,6 +2806,11 @@ void G_PlayerReborn(INT32 player, boolean betweenmaps)
 
 	if (p->mare == 255)
 		p->mare = 0;
+
+	if (!p->spectator && p->enteredgame)
+	{
+		ACS_RunPlayerRespawnScript(p);
+	}
 }
 
 //
@@ -4280,6 +4290,8 @@ static void G_DoStartContinue(void)
 
 	G_PlayerFinishLevel(consoleplayer); // take away cards and stuff
 
+	ACS_RunGameOverScript();
+
 	F_StartContinue();
 	gameaction = ga_nothing;
 }
diff --git a/src/g_game.h b/src/g_game.h
index 0d5fc7e373179f9b9905ab9361bb2dafb146afb7..fce963821ab89b7ac296aa21e1ccc0bf01205f83 100644
--- a/src/g_game.h
+++ b/src/g_game.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -21,6 +21,10 @@
 #include "m_cheat.h" // objectplacing
 #include "m_cond.h"
 
+#ifdef __cplusplus
+extern "C" {
+#endif
+
 extern char gamedatafilename[64];
 extern char timeattackfolder[64];
 extern char customversionstring[32];
@@ -274,4 +278,8 @@ FUNCMATH INT32 G_TicsToMilliseconds(tic_t tics);
 // Don't split up TOL handling
 UINT32 G_TOLFlag(INT32 pgametype);
 
+#ifdef __cplusplus
+} // extern "C"
+#endif
+
 #endif
diff --git a/src/hu_stuff.h b/src/hu_stuff.h
index ca77ed93002750d6cefa28584d8e4de7be3bfc65..c535fecef82339c4c54c607021fb7da160322637 100644
--- a/src/hu_stuff.h
+++ b/src/hu_stuff.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -18,6 +18,10 @@
 #include "w_wad.h"
 #include "r_defs.h"
 
+#ifdef __cplusplus
+extern "C" {
+#endif
+
 //------------------------------------
 //           Fonts & stuff
 //------------------------------------
@@ -123,4 +127,9 @@ void HU_DoCEcho(const char *msg);
 extern UINT32 hu_demoscore;
 extern UINT32 hu_demotime;
 extern UINT16 hu_demorings;
+
+#ifdef __cplusplus
+} // extern "C"
+#endif
+
 #endif
diff --git a/src/info.h b/src/info.h
index 0361f64281150bec03676bd1b8a4baa36a18de22..e6a3b26bbe64ad542c315784b29c7bacb88142bf 100644
--- a/src/info.h
+++ b/src/info.h
@@ -20,9 +20,15 @@
 #include "m_fixed.h"
 #include "dehacked.h" // MAX_ACTION_RECURSION
 
+#ifdef __cplusplus
+extern "C" {
+#endif
+
 // deh_tables.c now has lists for the more named enums! PLEASE keep them up to date!
 // For great modding!!
 
+struct mobj_s;
+
 // IMPORTANT!
 // DO NOT FORGET TO SYNC THIS LIST WITH THE ACTIONPOINTERS ARRAY IN DEH_TABLES.C
 enum actionnum
@@ -300,274 +306,274 @@ enum actionnum
 
 // IMPORTANT NOTE: If you add/remove from this list of action
 // functions, don't forget to update them in deh_tables.c!
-void A_Explode();
-void A_Pain();
-void A_Fall();
-void A_MonitorPop();
-void A_GoldMonitorPop();
-void A_GoldMonitorRestore();
-void A_GoldMonitorSparkle();
-void A_Look();
-void A_Chase();
-void A_FaceStabChase();
-void A_FaceStabRev();
-void A_FaceStabHurl();
-void A_FaceStabMiss();
-void A_StatueBurst();
-void A_FaceTarget();
-void A_FaceTracer();
-void A_Scream();
-void A_BossDeath();
-void A_SetShadowScale();
-void A_ShadowScream(); // MARIA!!!!!!
-void A_CustomPower(); // Use this for a custom power
-void A_GiveWeapon(); // Gives the player weapon(s)
-void A_RingBox(); // Obtained Ring Box Tails
-void A_Invincibility(); // Obtained Invincibility Box
-void A_SuperSneakers(); // Obtained Super Sneakers Box
-void A_BunnyHop(); // have bunny hop tails
-void A_BubbleSpawn(); // Randomly spawn bubbles
-void A_FanBubbleSpawn();
-void A_BubbleRise(); // Bubbles float to surface
-void A_BubbleCheck(); // Don't draw if not underwater
-void A_AwardScore();
-void A_ExtraLife(); // Extra Life
-void A_GiveShield(); // Obtained Shield
-void A_GravityBox();
-void A_ScoreRise(); // Rise the score logo
-void A_AttractChase(); // Ring Chase
-void A_DropMine(); // Drop Mine from Skim or Jetty-Syn Bomber
-void A_FishJump(); // Fish Jump
-void A_ThrownRing(); // Sparkle trail for red ring
-void A_SetSolidSteam();
-void A_UnsetSolidSteam();
-void A_SignSpin();
-void A_SignPlayer();
-void A_OverlayThink();
-void A_JetChase();
-void A_JetbThink(); // Jetty-Syn Bomber Thinker
-void A_JetgThink(); // Jetty-Syn Gunner Thinker
-void A_JetgShoot(); // Jetty-Syn Shoot Function
-void A_ShootBullet(); // JetgShoot without reactiontime setting
-void A_MinusDigging();
-void A_MinusPopup();
-void A_MinusCheck();
-void A_ChickenCheck();
-void A_MouseThink(); // Mouse Thinker
-void A_DetonChase(); // Deton Chaser
-void A_CapeChase(); // Fake little Super Sonic cape
-void A_RotateSpikeBall(); // Spike ball rotation
-void A_SlingAppear();
-void A_UnidusBall();
-void A_RockSpawn();
-void A_SetFuse();
-void A_CrawlaCommanderThink(); // Crawla Commander
-void A_SmokeTrailer();
-void A_RingExplode();
-void A_OldRingExplode();
-void A_MixUp();
-void A_RecyclePowers();
-void A_BossScream();
-void A_Boss2TakeDamage();
-void A_GoopSplat();
-void A_Boss2PogoSFX();
-void A_Boss2PogoTarget();
-void A_EggmanBox();
-void A_TurretFire();
-void A_SuperTurretFire();
-void A_TurretStop();
-void A_JetJawRoam();
-void A_JetJawChomp();
-void A_PointyThink();
-void A_CheckBuddy();
-void A_HoodFire();
-void A_HoodThink();
-void A_HoodFall();
-void A_ArrowBonks();
-void A_SnailerThink();
-void A_SharpChase();
-void A_SharpSpin();
-void A_SharpDecel();
-void A_CrushstaceanWalk();
-void A_CrushstaceanPunch();
-void A_CrushclawAim();
-void A_CrushclawLaunch();
-void A_VultureVtol();
-void A_VultureCheck();
-void A_VultureHover();
-void A_VultureBlast();
-void A_VultureFly();
-void A_SkimChase();
-void A_SkullAttack();
-void A_LobShot();
-void A_FireShot();
-void A_SuperFireShot();
-void A_BossFireShot();
-void A_Boss7FireMissiles();
-void A_Boss1Laser();
-void A_FocusTarget();
-void A_Boss4Reverse();
-void A_Boss4SpeedUp();
-void A_Boss4Raise();
-void A_SparkFollow();
-void A_BuzzFly();
-void A_GuardChase();
-void A_EggShield();
-void A_SetReactionTime();
-void A_Boss1Spikeballs();
-void A_Boss3TakeDamage();
-void A_Boss3Path();
-void A_Boss3ShockThink();
-void A_Shockwave();
-void A_LinedefExecute();
-void A_LinedefExecuteFromArg();
-void A_PlaySeeSound();
-void A_PlayAttackSound();
-void A_PlayActiveSound();
-void A_1upThinker();
-void A_BossZoom(); //Unused
-void A_Boss1Chase();
-void A_Boss2Chase();
-void A_Boss2Pogo();
-void A_Boss7Chase();
-void A_BossJetFume();
-void A_SpawnObjectAbsolute();
-void A_SpawnObjectRelative();
-void A_ChangeAngleRelative();
-void A_ChangeAngleAbsolute();
-void A_RollAngle();
-void A_ChangeRollAngleRelative();
-void A_ChangeRollAngleAbsolute();
-void A_PlaySound();
-void A_FindTarget();
-void A_FindTracer();
-void A_SetTics();
-void A_SetRandomTics();
-void A_ChangeColorRelative();
-void A_ChangeColorAbsolute();
-void A_Dye();
-void A_SetTranslation();
-void A_MoveRelative();
-void A_MoveAbsolute();
-void A_Thrust();
-void A_ZThrust();
-void A_SetTargetsTarget();
-void A_SetObjectFlags();
-void A_SetObjectFlags2();
-void A_RandomState();
-void A_RandomStateRange();
-void A_StateRangeByAngle();
-void A_StateRangeByParameter();
-void A_DualAction();
-void A_RemoteAction();
-void A_ToggleFlameJet();
-void A_OrbitNights();
-void A_GhostMe();
-void A_SetObjectState();
-void A_SetObjectTypeState();
-void A_KnockBack();
-void A_PushAway();
-void A_RingDrain();
-void A_SplitShot();
-void A_MissileSplit();
-void A_MultiShot();
-void A_InstaLoop();
-void A_Custom3DRotate();
-void A_SearchForPlayers();
-void A_CheckRandom();
-void A_CheckTargetRings();
-void A_CheckRings();
-void A_CheckTotalRings();
-void A_CheckHealth();
-void A_CheckRange();
-void A_CheckHeight();
-void A_CheckTrueRange();
-void A_CheckThingCount();
-void A_CheckAmbush();
-void A_CheckCustomValue();
-void A_CheckCusValMemo();
-void A_SetCustomValue();
-void A_UseCusValMemo();
-void A_RelayCustomValue();
-void A_CusValAction();
-void A_ForceStop();
-void A_ForceWin();
-void A_SpikeRetract();
-void A_InfoState();
-void A_Repeat();
-void A_SetScale();
-void A_RemoteDamage();
-void A_HomingChase();
-void A_TrapShot();
-void A_VileTarget();
-void A_VileAttack();
-void A_VileFire();
-void A_BrakChase();
-void A_BrakFireShot();
-void A_BrakLobShot();
-void A_NapalmScatter();
-void A_SpawnFreshCopy();
-void A_FlickySpawn();
-void A_FlickyCenter();
-void A_FlickyAim();
-void A_FlickyFly();
-void A_FlickySoar();
-void A_FlickyCoast();
-void A_FlickyHop();
-void A_FlickyFlounder();
-void A_FlickyCheck();
-void A_FlickyHeightCheck();
-void A_FlickyFlutter();
-void A_FlameParticle();
-void A_FadeOverlay();
-void A_Boss5Jump();
-void A_LightBeamReset();
-void A_MineExplode();
-void A_MineRange();
-void A_ConnectToGround();
-void A_SpawnParticleRelative();
-void A_MultiShotDist();
-void A_WhoCaresIfYourSonIsABee();
-void A_ParentTriesToSleep();
-void A_CryingToMomma();
-void A_CheckFlags2();
-void A_Boss5FindWaypoint();
-void A_DoNPCSkid();
-void A_DoNPCPain();
-void A_PrepareRepeat();
-void A_Boss5ExtraRepeat();
-void A_Boss5Calm();
-void A_Boss5CheckOnGround();
-void A_Boss5CheckFalling();
-void A_Boss5PinchShot();
-void A_Boss5MakeItRain();
-void A_Boss5MakeJunk();
-void A_LookForBetter();
-void A_Boss5BombExplode();
-void A_DustDevilThink();
-void A_TNTExplode();
-void A_DebrisRandom();
-void A_TrainCameo();
-void A_TrainCameo2();
-void A_CanarivoreGas();
-void A_KillSegments();
-void A_SnapperSpawn();
-void A_SnapperThinker();
-void A_SaloonDoorSpawn();
-void A_MinecartSparkThink();
-void A_ModuloToState();
-void A_LavafallRocks();
-void A_LavafallLava();
-void A_FallingLavaCheck();
-void A_FireShrink();
-void A_SpawnPterabytes();
-void A_PterabyteHover();
-void A_RolloutSpawn();
-void A_RolloutRock();
-void A_DragonbomberSpawn();
-void A_DragonWing();
-void A_DragonSegment();
-void A_ChangeHeight();
+void A_Explode(struct mobj_s *actor);
+void A_Pain(struct mobj_s *actor);
+void A_Fall(struct mobj_s *actor);
+void A_MonitorPop(struct mobj_s *actor);
+void A_GoldMonitorPop(struct mobj_s *actor);
+void A_GoldMonitorRestore(struct mobj_s *actor);
+void A_GoldMonitorSparkle(struct mobj_s *actor);
+void A_Look(struct mobj_s *actor);
+void A_Chase(struct mobj_s *actor);
+void A_FaceStabChase(struct mobj_s *actor);
+void A_FaceStabRev(struct mobj_s *actor);
+void A_FaceStabHurl(struct mobj_s *actor);
+void A_FaceStabMiss(struct mobj_s *actor);
+void A_StatueBurst(struct mobj_s *actor);
+void A_FaceTarget(struct mobj_s *actor);
+void A_FaceTracer(struct mobj_s *actor);
+void A_Scream(struct mobj_s *actor);
+void A_BossDeath(struct mobj_s *actor);
+void A_SetShadowScale(struct mobj_s *actor);
+void A_ShadowScream(struct mobj_s *actor); // MARIA!!!!!!
+void A_CustomPower(struct mobj_s *actor); // Use this for a custom power
+void A_GiveWeapon(struct mobj_s *actor); // Gives the player weapon(s)
+void A_RingBox(struct mobj_s *actor); // Obtained Ring Box Tails
+void A_Invincibility(struct mobj_s *actor); // Obtained Invincibility Box
+void A_SuperSneakers(struct mobj_s *actor); // Obtained Super Sneakers Box
+void A_BunnyHop(struct mobj_s *actor); // have bunny hop tails
+void A_BubbleSpawn(struct mobj_s *actor); // Randomly spawn bubbles
+void A_FanBubbleSpawn(struct mobj_s *actor);
+void A_BubbleRise(struct mobj_s *actor); // Bubbles float to surface
+void A_BubbleCheck(struct mobj_s *actor); // Don't draw if not underwater
+void A_AwardScore(struct mobj_s *actor);
+void A_ExtraLife(struct mobj_s *actor); // Extra Life
+void A_GiveShield(struct mobj_s *actor); // Obtained Shield
+void A_GravityBox(struct mobj_s *actor);
+void A_ScoreRise(struct mobj_s *actor); // Rise the score logo
+void A_AttractChase(struct mobj_s *actor); // Ring Chase
+void A_DropMine(struct mobj_s *actor); // Drop Mine from Skim or Jetty-Syn Bomber
+void A_FishJump(struct mobj_s *actor); // Fish Jump
+void A_ThrownRing(struct mobj_s *actor); // Sparkle trail for red ring
+void A_SetSolidSteam(struct mobj_s *actor);
+void A_UnsetSolidSteam(struct mobj_s *actor);
+void A_SignSpin(struct mobj_s *actor);
+void A_SignPlayer(struct mobj_s *actor);
+void A_OverlayThink(struct mobj_s *actor);
+void A_JetChase(struct mobj_s *actor);
+void A_JetbThink(struct mobj_s *actor); // Jetty-Syn Bomber Thinker
+void A_JetgThink(struct mobj_s *actor); // Jetty-Syn Gunner Thinker
+void A_JetgShoot(struct mobj_s *actor); // Jetty-Syn Shoot Function
+void A_ShootBullet(struct mobj_s *actor); // JetgShoot without reactiontime setting
+void A_MinusDigging(struct mobj_s *actor);
+void A_MinusPopup(struct mobj_s *actor);
+void A_MinusCheck(struct mobj_s *actor);
+void A_ChickenCheck(struct mobj_s *actor);
+void A_MouseThink(struct mobj_s *actor); // Mouse Thinker
+void A_DetonChase(struct mobj_s *actor); // Deton Chaser
+void A_CapeChase(struct mobj_s *actor); // Fake little Super Sonic cape
+void A_RotateSpikeBall(struct mobj_s *actor); // Spike ball rotation
+void A_SlingAppear(struct mobj_s *actor);
+void A_UnidusBall(struct mobj_s *actor);
+void A_RockSpawn(struct mobj_s *actor);
+void A_SetFuse(struct mobj_s *actor);
+void A_CrawlaCommanderThink(struct mobj_s *actor); // Crawla Commander
+void A_SmokeTrailer(struct mobj_s *actor);
+void A_RingExplode(struct mobj_s *actor);
+void A_OldRingExplode(struct mobj_s *actor);
+void A_MixUp(struct mobj_s *actor);
+void A_RecyclePowers(struct mobj_s *actor);
+void A_BossScream(struct mobj_s *actor);
+void A_Boss2TakeDamage(struct mobj_s *actor);
+void A_GoopSplat(struct mobj_s *actor);
+void A_Boss2PogoSFX(struct mobj_s *actor);
+void A_Boss2PogoTarget(struct mobj_s *actor);
+void A_EggmanBox(struct mobj_s *actor);
+void A_TurretFire(struct mobj_s *actor);
+void A_SuperTurretFire(struct mobj_s *actor);
+void A_TurretStop(struct mobj_s *actor);
+void A_JetJawRoam(struct mobj_s *actor);
+void A_JetJawChomp(struct mobj_s *actor);
+void A_PointyThink(struct mobj_s *actor);
+void A_CheckBuddy(struct mobj_s *actor);
+void A_HoodFire(struct mobj_s *actor);
+void A_HoodThink(struct mobj_s *actor);
+void A_HoodFall(struct mobj_s *actor);
+void A_ArrowBonks(struct mobj_s *actor);
+void A_SnailerThink(struct mobj_s *actor);
+void A_SharpChase(struct mobj_s *actor);
+void A_SharpSpin(struct mobj_s *actor);
+void A_SharpDecel(struct mobj_s *actor);
+void A_CrushstaceanWalk(struct mobj_s *actor);
+void A_CrushstaceanPunch(struct mobj_s *actor);
+void A_CrushclawAim(struct mobj_s *actor);
+void A_CrushclawLaunch(struct mobj_s *actor);
+void A_VultureVtol(struct mobj_s *actor);
+void A_VultureCheck(struct mobj_s *actor);
+void A_VultureHover(struct mobj_s *actor);
+void A_VultureBlast(struct mobj_s *actor);
+void A_VultureFly(struct mobj_s *actor);
+void A_SkimChase(struct mobj_s *actor);
+void A_SkullAttack(struct mobj_s *actor);
+void A_LobShot(struct mobj_s *actor);
+void A_FireShot(struct mobj_s *actor);
+void A_SuperFireShot(struct mobj_s *actor);
+void A_BossFireShot(struct mobj_s *actor);
+void A_Boss7FireMissiles(struct mobj_s *actor);
+void A_Boss1Laser(struct mobj_s *actor);
+void A_FocusTarget(struct mobj_s *actor);
+void A_Boss4Reverse(struct mobj_s *actor);
+void A_Boss4SpeedUp(struct mobj_s *actor);
+void A_Boss4Raise(struct mobj_s *actor);
+void A_SparkFollow(struct mobj_s *actor);
+void A_BuzzFly(struct mobj_s *actor);
+void A_GuardChase(struct mobj_s *actor);
+void A_EggShield(struct mobj_s *actor);
+void A_SetReactionTime(struct mobj_s *actor);
+void A_Boss1Spikeballs(struct mobj_s *actor);
+void A_Boss3TakeDamage(struct mobj_s *actor);
+void A_Boss3Path(struct mobj_s *actor);
+void A_Boss3ShockThink(struct mobj_s *actor);
+void A_Shockwave(struct mobj_s *actor);
+void A_LinedefExecute(struct mobj_s *actor);
+void A_LinedefExecuteFromArg(struct mobj_s *actor);
+void A_PlaySeeSound(struct mobj_s *actor);
+void A_PlayAttackSound(struct mobj_s *actor);
+void A_PlayActiveSound(struct mobj_s *actor);
+void A_1upThinker(struct mobj_s *actor);
+void A_BossZoom(struct mobj_s *actor); //Unused
+void A_Boss1Chase(struct mobj_s *actor);
+void A_Boss2Chase(struct mobj_s *actor);
+void A_Boss2Pogo(struct mobj_s *actor);
+void A_Boss7Chase(struct mobj_s *actor);
+void A_BossJetFume(struct mobj_s *actor);
+void A_SpawnObjectAbsolute(struct mobj_s *actor);
+void A_SpawnObjectRelative(struct mobj_s *actor);
+void A_ChangeAngleRelative(struct mobj_s *actor);
+void A_ChangeAngleAbsolute(struct mobj_s *actor);
+void A_RollAngle(struct mobj_s *actor);
+void A_ChangeRollAngleRelative(struct mobj_s *actor);
+void A_ChangeRollAngleAbsolute(struct mobj_s *actor);
+void A_PlaySound(struct mobj_s *actor);
+void A_FindTarget(struct mobj_s *actor);
+void A_FindTracer(struct mobj_s *actor);
+void A_SetTics(struct mobj_s *actor);
+void A_SetRandomTics(struct mobj_s *actor);
+void A_ChangeColorRelative(struct mobj_s *actor);
+void A_ChangeColorAbsolute(struct mobj_s *actor);
+void A_Dye(struct mobj_s *actor);
+void A_SetTranslation(struct mobj_s *actor);
+void A_MoveRelative(struct mobj_s *actor);
+void A_MoveAbsolute(struct mobj_s *actor);
+void A_Thrust(struct mobj_s *actor);
+void A_ZThrust(struct mobj_s *actor);
+void A_SetTargetsTarget(struct mobj_s *actor);
+void A_SetObjectFlags(struct mobj_s *actor);
+void A_SetObjectFlags2(struct mobj_s *actor);
+void A_RandomState(struct mobj_s *actor);
+void A_RandomStateRange(struct mobj_s *actor);
+void A_StateRangeByAngle(struct mobj_s *actor);
+void A_StateRangeByParameter(struct mobj_s *actor);
+void A_DualAction(struct mobj_s *actor);
+void A_RemoteAction(struct mobj_s *actor);
+void A_ToggleFlameJet(struct mobj_s *actor);
+void A_OrbitNights(struct mobj_s *actor);
+void A_GhostMe(struct mobj_s *actor);
+void A_SetObjectState(struct mobj_s *actor);
+void A_SetObjectTypeState(struct mobj_s *actor);
+void A_KnockBack(struct mobj_s *actor);
+void A_PushAway(struct mobj_s *actor);
+void A_RingDrain(struct mobj_s *actor);
+void A_SplitShot(struct mobj_s *actor);
+void A_MissileSplit(struct mobj_s *actor);
+void A_MultiShot(struct mobj_s *actor);
+void A_InstaLoop(struct mobj_s *actor);
+void A_Custom3DRotate(struct mobj_s *actor);
+void A_SearchForPlayers(struct mobj_s *actor);
+void A_CheckRandom(struct mobj_s *actor);
+void A_CheckTargetRings(struct mobj_s *actor);
+void A_CheckRings(struct mobj_s *actor);
+void A_CheckTotalRings(struct mobj_s *actor);
+void A_CheckHealth(struct mobj_s *actor);
+void A_CheckRange(struct mobj_s *actor);
+void A_CheckHeight(struct mobj_s *actor);
+void A_CheckTrueRange(struct mobj_s *actor);
+void A_CheckThingCount(struct mobj_s *actor);
+void A_CheckAmbush(struct mobj_s *actor);
+void A_CheckCustomValue(struct mobj_s *actor);
+void A_CheckCusValMemo(struct mobj_s *actor);
+void A_SetCustomValue(struct mobj_s *actor);
+void A_UseCusValMemo(struct mobj_s *actor);
+void A_RelayCustomValue(struct mobj_s *actor);
+void A_CusValAction(struct mobj_s *actor);
+void A_ForceStop(struct mobj_s *actor);
+void A_ForceWin(struct mobj_s *actor);
+void A_SpikeRetract(struct mobj_s *actor);
+void A_InfoState(struct mobj_s *actor);
+void A_Repeat(struct mobj_s *actor);
+void A_SetScale(struct mobj_s *actor);
+void A_RemoteDamage(struct mobj_s *actor);
+void A_HomingChase(struct mobj_s *actor);
+void A_TrapShot(struct mobj_s *actor);
+void A_VileTarget(struct mobj_s *actor);
+void A_VileAttack(struct mobj_s *actor);
+void A_VileFire(struct mobj_s *actor);
+void A_BrakChase(struct mobj_s *actor);
+void A_BrakFireShot(struct mobj_s *actor);
+void A_BrakLobShot(struct mobj_s *actor);
+void A_NapalmScatter(struct mobj_s *actor);
+void A_SpawnFreshCopy(struct mobj_s *actor);
+void A_FlickySpawn(struct mobj_s *actor);
+void A_FlickyCenter(struct mobj_s *actor);
+void A_FlickyAim(struct mobj_s *actor);
+void A_FlickyFly(struct mobj_s *actor);
+void A_FlickySoar(struct mobj_s *actor);
+void A_FlickyCoast(struct mobj_s *actor);
+void A_FlickyHop(struct mobj_s *actor);
+void A_FlickyFlounder(struct mobj_s *actor);
+void A_FlickyCheck(struct mobj_s *actor);
+void A_FlickyHeightCheck(struct mobj_s *actor);
+void A_FlickyFlutter(struct mobj_s *actor);
+void A_FlameParticle(struct mobj_s *actor);
+void A_FadeOverlay(struct mobj_s *actor);
+void A_Boss5Jump(struct mobj_s *actor);
+void A_LightBeamReset(struct mobj_s *actor);
+void A_MineExplode(struct mobj_s *actor);
+void A_MineRange(struct mobj_s *actor);
+void A_ConnectToGround(struct mobj_s *actor);
+void A_SpawnParticleRelative(struct mobj_s *actor);
+void A_MultiShotDist(struct mobj_s *actor);
+void A_WhoCaresIfYourSonIsABee(struct mobj_s *actor);
+void A_ParentTriesToSleep(struct mobj_s *actor);
+void A_CryingToMomma(struct mobj_s *actor);
+void A_CheckFlags2(struct mobj_s *actor);
+void A_Boss5FindWaypoint(struct mobj_s *actor);
+void A_DoNPCSkid(struct mobj_s *actor);
+void A_DoNPCPain(struct mobj_s *actor);
+void A_PrepareRepeat(struct mobj_s *actor);
+void A_Boss5ExtraRepeat(struct mobj_s *actor);
+void A_Boss5Calm(struct mobj_s *actor);
+void A_Boss5CheckOnGround(struct mobj_s *actor);
+void A_Boss5CheckFalling(struct mobj_s *actor);
+void A_Boss5PinchShot(struct mobj_s *actor);
+void A_Boss5MakeItRain(struct mobj_s *actor);
+void A_Boss5MakeJunk(struct mobj_s *actor);
+void A_LookForBetter(struct mobj_s *actor);
+void A_Boss5BombExplode(struct mobj_s *actor);
+void A_DustDevilThink(struct mobj_s *actor);
+void A_TNTExplode(struct mobj_s *actor);
+void A_DebrisRandom(struct mobj_s *actor);
+void A_TrainCameo(struct mobj_s *actor);
+void A_TrainCameo2(struct mobj_s *actor);
+void A_CanarivoreGas(struct mobj_s *actor);
+void A_KillSegments(struct mobj_s *actor);
+void A_SnapperSpawn(struct mobj_s *actor);
+void A_SnapperThinker(struct mobj_s *actor);
+void A_SaloonDoorSpawn(struct mobj_s *actor);
+void A_MinecartSparkThink(struct mobj_s *actor);
+void A_ModuloToState(struct mobj_s *actor);
+void A_LavafallRocks(struct mobj_s *actor);
+void A_LavafallLava(struct mobj_s *actor);
+void A_FallingLavaCheck(struct mobj_s *actor);
+void A_FireShrink(struct mobj_s *actor);
+void A_SpawnPterabytes(struct mobj_s *actor);
+void A_PterabyteHover(struct mobj_s *actor);
+void A_RolloutSpawn(struct mobj_s *actor);
+void A_RolloutRock(struct mobj_s *actor);
+void A_DragonbomberSpawn(struct mobj_s *actor);
+void A_DragonWing(struct mobj_s *actor);
+void A_DragonSegment(struct mobj_s *actor);
+void A_ChangeHeight(struct mobj_s *actor);
 
 extern int actionsoverridden[NUMACTIONS][MAX_ACTION_RECURSION];
 
@@ -5205,4 +5211,8 @@ void P_BackupTables(void);
 
 void P_ResetData(INT32 flags);
 
+#ifdef __cplusplus
+} // extern "C"
+#endif
+
 #endif
diff --git a/src/lua_baselib.c b/src/lua_baselib.c
index f6b8f462b5a41cee30c33145fd62fa28daa01e92..9ad504668d765beadf45b069ed8602df10bc47c9 100644
--- a/src/lua_baselib.c
+++ b/src/lua_baselib.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 2012-2016 by John "JTE" Muniz.
-// Copyright (C) 2012-2023 by Sonic Team Junior.
+// Copyright (C) 2012-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -216,8 +216,14 @@ static const struct {
 	{META_LINEARGS,     "line_t.args"},
 	{META_LINESTRINGARGS, "line_t.stringargs"},
 
+	{META_SECTORARGS,     "sector_t.args"},
+	{META_SECTORSTRINGARGS, "sector_t.stringargs"},
+
 	{META_THINGARGS,     "mapthing.args"},
 	{META_THINGSTRINGARGS, "mapthing.stringargs"},
+
+	{META_THINGSPECIALARGS,     "mapthing.specialargs"},
+	{META_THINGSPECIALSTRINGARGS, "mapthing.specialstringargs"},
 #ifdef HAVE_LUA_SEGS
 	{META_NODEBBOX,     "node_t.bbox"},
 	{META_NODECHILDREN, "node_t.children"},
@@ -3860,80 +3866,32 @@ static int lib_gAddGametype(lua_State *L)
 // Partly lifted from Got_AddPlayer
 static int lib_gAddPlayer(lua_State *L)
 {
-	INT16 i, newplayernum;
-	player_t *newplayer;
-	SINT8 skinnum = 0, bot;
-
-	for (i = 0; i < MAXPLAYERS; i++)
-	{
-		if (!playeringame[i])
-			break;
-	}
-
-	if (i >= MAXPLAYERS)
-	{
-		lua_pushnil(L);
-		return 1;
-	}
-
-	newplayernum = i;
-
-	CL_ClearPlayer(newplayernum);
-
-	playeringame[newplayernum] = true;
-	G_AddPlayer(newplayernum);
-	newplayer = &players[newplayernum];
+	const char *skinname = NULL;
+	UINT16 skincolor = SKINCOLOR_NONE;
+	const char *botname = NULL;
+	SINT8 bottype;
 
-	newplayer->jointime = 0;
-	newplayer->quittime = 0;
-	newplayer->lastinputtime = 0;
-
-	// Read the skin argument (defaults to Sonic)
+	// Read the skin argument
 	if (!lua_isnoneornil(L, 1))
-	{
-		skinnum = R_SkinAvailable(luaL_checkstring(L, 1));
-		skinnum = skinnum < 0 ? 0 : skinnum;
-	}
+		skinname = luaL_checkstring(L, 1);
 
-	// Read the color (defaults to skin prefcolor)
+	// Read the color
 	if (!lua_isnoneornil(L, 2))
-		newplayer->skincolor = R_GetColorByName(luaL_checkstring(L, 2));
-	else
-		newplayer->skincolor = skins[skinnum]->prefcolor;
-
-	// Set the bot default name as the skin
-	strcpy(player_names[newplayernum], skins[skinnum]->realname);
+		skincolor = R_GetColorByName(luaL_checkstring(L, 2));
 
 	// Read the bot name, if given
 	if (!lua_isnoneornil(L, 3))
-		strlcpy(player_names[newplayernum], luaL_checkstring(L, 3), sizeof(*player_names));
-
-	bot = luaL_optinteger(L, 4, 3);
-	newplayer->bot = (bot >= BOT_NONE && bot <= BOT_MPAI) ? bot : BOT_MPAI;
-
-	// If our bot is a 2P type, we'll need to set its leader so it can spawn
-	if (newplayer->bot == BOT_2PAI || newplayer->bot == BOT_2PHUMAN)
-		B_UpdateBotleader(newplayer);
+		botname = luaL_checkstring(L, 3);
 
-	// Set the skin (can't do this until AFTER bot type is set!)
-	SetPlayerSkinByNum(newplayernum, skinnum);
+	bottype = luaL_optinteger(L, 4, 3);
 
-	if (netgame)
-	{
-		char joinmsg[256];
-
-		// Truncate bot name
-		player_names[newplayernum][sizeof(*player_names) - 8] = '\0'; // The length of colored [BOT] + 1
-
-		strcpy(joinmsg, M_GetText("\x82*Bot %s has joined the game (player %d)"));
-		strcpy(joinmsg, va(joinmsg, player_names[newplayernum], newplayernum));
-		HU_AddChatText(joinmsg, false);
-
-		// Append blue [BOT] tag at the end
-		strlcat(player_names[newplayernum], "\x84[BOT]\x80", sizeof(*player_names));
+	INT16 playernum = B_AddBot(skinname, skincolor, botname, bottype);
+	if (playernum < 0) {
+		lua_pushnil(L);
+		return 1;
 	}
 
-	LUA_PushUserdata(L, newplayer, META_PLAYER);
+	LUA_PushUserdata(L, &players[playernum], META_PLAYER);
 	return 1;
 }
 
diff --git a/src/lua_libs.h b/src/lua_libs.h
index a90d8ac7fb800c426e7bef94538803366a835b8b..931f196dc398df23ff862901eaa31ea2939df5ef 100644
--- a/src/lua_libs.h
+++ b/src/lua_libs.h
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 2012-2016 by John "JTE" Muniz.
-// Copyright (C) 2012-2023 by Sonic Team Junior.
+// Copyright (C) 2012-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -71,8 +71,12 @@ extern boolean ignoregameinputs;
 #define META_SIDENUM "LINE_T*SIDENUM"
 #define META_LINEARGS "LINE_T*ARGS"
 #define META_LINESTRINGARGS "LINE_T*STRINGARGS"
+#define META_SECTORARGS "SECTOR_T*ARGS"
+#define META_SECTORSTRINGARGS "SECTOR_T*STRINGARGS"
 #define META_THINGARGS "MAPTHING_T*ARGS"
 #define META_THINGSTRINGARGS "MAPTHING_T*STRINGARGS"
+#define META_THINGSPECIALARGS "MAPTHING_T*SPECIALARGS"
+#define META_THINGSPECIALSTRINGARGS "MAPTHING_T*SPECIALSTRINGARGS"
 #define META_POLYOBJVERTICES "POLYOBJ_T*VERTICES"
 #define META_POLYOBJLINES "POLYOBJ_T*LINES"
 #ifdef HAVE_LUA_SEGS
diff --git a/src/lua_maplib.c b/src/lua_maplib.c
index 6b489f22b1939941c0ebf83a5a3ae0b7e215de5a..328a8b6e5220035033d7e6d058e5dbb1dfb4802f 100644
--- a/src/lua_maplib.c
+++ b/src/lua_maplib.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 2012-2016 by John "JTE" Muniz.
-// Copyright (C) 2012-2023 by Sonic Team Junior.
+// Copyright (C) 2012-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -69,6 +69,10 @@ enum sector_e {
 	sector_triggerer,
 	sector_friction,
 	sector_gravity,
+	sector_action,
+	sector_args,
+	sector_stringargs,
+	sector_activation,
 };
 
 static const char *const sector_opt[] = {
@@ -112,6 +116,10 @@ static const char *const sector_opt[] = {
 	"triggerer",
 	"friction",
 	"gravity",
+	"action",
+	"args",
+	"stringargs",
+	"activation",
 	NULL};
 
 static int sector_fields_ref = LUA_NOREF;
@@ -142,6 +150,7 @@ enum line_e {
 	line_dy,
 	line_angle,
 	line_flags,
+	line_activation,
 	line_special,
 	line_tag,
 	line_taglist,
@@ -168,6 +177,7 @@ static const char *const line_opt[] = {
 	"dy",
 	"angle",
 	"flags",
+	"activation",
 	"special",
 	"tag",
 	"taglist",
@@ -662,6 +672,42 @@ static int sectorlines_num(lua_State *L)
 // sector_t //
 //////////////
 
+// args, i -> args[i]
+static int sectorargs_get(lua_State *L)
+{
+	INT32 *args = *((INT32**)luaL_checkudata(L, 1, META_SECTORARGS));
+	int i = luaL_checkinteger(L, 2);
+	if (i < 0 || i >= NUM_SCRIPT_ARGS)
+		return luaL_error(L, LUA_QL("sector_t.args") " index cannot be %d", i);
+	lua_pushinteger(L, args[i]);
+	return 1;
+}
+
+// #args -> NUM_SCRIPT_ARGS
+static int sectorargs_len(lua_State* L)
+{
+	lua_pushinteger(L, NUM_SCRIPT_ARGS);
+	return 1;
+}
+
+// stringargs, i -> stringargs[i]
+static int sectorstringargs_get(lua_State *L)
+{
+	char **stringargs = *((char***)luaL_checkudata(L, 1, META_SECTORSTRINGARGS));
+	int i = luaL_checkinteger(L, 2);
+	if (i < 0 || i >= NUM_SCRIPT_STRINGARGS)
+		return luaL_error(L, LUA_QL("sector_t.stringargs") " index cannot be %d", i);
+	lua_pushstring(L, stringargs[i]);
+	return 1;
+}
+
+// #stringargs -> NUM_SCRIPT_STRINGARGS
+static int sectorstringargs_len(lua_State *L)
+{
+	lua_pushinteger(L, NUM_SCRIPT_STRINGARGS);
+	return 1;
+}
+
 static int sector_get(lua_State *L)
 {
 	sector_t *sector = *((sector_t **)luaL_checkudata(L, 1, META_SECTOR));
@@ -819,6 +865,18 @@ static int sector_get(lua_State *L)
 	case sector_gravity: // gravity
 		lua_pushfixed(L, sector->gravity);
 		return 1;
+	case sector_action: // action
+		lua_pushinteger(L, (INT16)sector->action);
+		return 1;
+	case sector_args:
+		LUA_PushUserdata(L, sector->args, META_SECTORARGS);
+		return 1;
+	case sector_stringargs:
+		LUA_PushUserdata(L, sector->stringargs, META_SECTORSTRINGARGS);
+		return 1;
+	case sector_activation:
+		lua_pushinteger(L, sector->activation);
+		return 1;
 	}
 	return 0;
 }
@@ -847,6 +905,8 @@ static int sector_set(lua_State *L)
 	case sector_fslope: // f_slope
 	case sector_cslope: // c_slope
 	case sector_friction: // friction
+	case sector_args: // args
+	case sector_stringargs: // stringargs
 		return luaL_error(L, "sector_t field " LUA_QS " cannot be set.", sector_opt[field]);
 	default:
 		return luaL_error(L, "sector_t has no field named " LUA_QS ".", lua_tostring(L, 2));
@@ -962,6 +1022,12 @@ static int sector_set(lua_State *L)
 	case sector_gravity:
 		sector->gravity = luaL_checkfixed(L, 3);
 		break;
+	case sector_action:
+		sector->action = (INT16)luaL_checkinteger(L, 3);
+		break;
+	case sector_activation:
+		sector->activation = luaL_checkinteger(L, 3);
+		break;
 	}
 	return 0;
 }
@@ -1030,16 +1096,16 @@ static int lineargs_get(lua_State *L)
 {
 	INT32 *args = *((INT32**)luaL_checkudata(L, 1, META_LINEARGS));
 	int i = luaL_checkinteger(L, 2);
-	if (i < 0 || i >= NUMLINEARGS)
+	if (i < 0 || i >= NUM_SCRIPT_ARGS)
 		return luaL_error(L, LUA_QL("line_t.args") " index cannot be %d", i);
 	lua_pushinteger(L, args[i]);
 	return 1;
 }
 
-// #args -> NUMLINEARGS
+// #args -> NUM_SCRIPT_ARGS
 static int lineargs_len(lua_State* L)
 {
-	lua_pushinteger(L, NUMLINEARGS);
+	lua_pushinteger(L, NUM_SCRIPT_ARGS);
 	return 1;
 }
 
@@ -1048,16 +1114,16 @@ static int linestringargs_get(lua_State *L)
 {
 	char **stringargs = *((char***)luaL_checkudata(L, 1, META_LINESTRINGARGS));
 	int i = luaL_checkinteger(L, 2);
-	if (i < 0 || i >= NUMLINESTRINGARGS)
+	if (i < 0 || i >= NUM_SCRIPT_STRINGARGS)
 		return luaL_error(L, LUA_QL("line_t.stringargs") " index cannot be %d", i);
 	lua_pushstring(L, stringargs[i]);
 	return 1;
 }
 
-// #stringargs -> NUMLINESTRINGARGS
+// #stringargs -> NUM_SCRIPT_STRINGARGS
 static int linestringargs_len(lua_State *L)
 {
-	lua_pushinteger(L, NUMLINESTRINGARGS);
+	lua_pushinteger(L, NUM_SCRIPT_STRINGARGS);
 	return 1;
 }
 
@@ -1098,6 +1164,9 @@ static int line_get(lua_State *L)
 	case line_flags:
 		lua_pushinteger(L, line->flags);
 		return 1;
+	case line_activation:
+		lua_pushinteger(L, line->activation);
+		return 1;
 	case line_special:
 		lua_pushinteger(L, line->special);
 		return 1;
@@ -3000,6 +3069,8 @@ int LUA_MapLib(lua_State *L)
 	LUA_RegisterUserdataMetatable(L, META_LINE, line_get, NULL, line_num);
 	LUA_RegisterUserdataMetatable(L, META_LINEARGS, lineargs_get, NULL, lineargs_len);
 	LUA_RegisterUserdataMetatable(L, META_LINESTRINGARGS, linestringargs_get, NULL, linestringargs_len);
+	LUA_RegisterUserdataMetatable(L, META_SECTORARGS, sectorargs_get, NULL, sectorargs_len);
+	LUA_RegisterUserdataMetatable(L, META_SECTORSTRINGARGS, sectorstringargs_get, NULL, sectorstringargs_len);
 	LUA_RegisterUserdataMetatable(L, META_SIDENUM, sidenum_get, NULL, NULL);
 	LUA_RegisterUserdataMetatable(L, META_SIDE, side_get, side_set, side_num);
 	LUA_RegisterUserdataMetatable(L, META_VERTEX, vertex_get, NULL, vertex_num);
diff --git a/src/lua_mobjlib.c b/src/lua_mobjlib.c
index de7065790abb137e82f505c961215ca6acb10429..380f279f976f7387f88a0a4933bdadeee6b43a8c 100644
--- a/src/lua_mobjlib.c
+++ b/src/lua_mobjlib.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 2012-2016 by John "JTE" Muniz.
-// Copyright (C) 2012-2023 by Sonic Team Junior.
+// Copyright (C) 2012-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -102,7 +102,13 @@ enum mobj_e {
 	mobj_colorized,
 	mobj_mirrored,
 	mobj_shadowscale,
-	mobj_dispoffset
+	mobj_dispoffset,
+	mobj_tid,
+	mobj_args,
+	mobj_stringargs,
+	mobj_special,
+	mobj_specialargs,
+	mobj_specialstringargs
 };
 
 static const char *const mobj_opt[] = {
@@ -184,6 +190,12 @@ static const char *const mobj_opt[] = {
 	"mirrored",
 	"shadowscale",
 	"dispoffset",
+	"tid",
+	"args",
+	"stringargs",
+	"special",
+	"specialargs",
+	"specialstringargs",
 	NULL};
 
 #define UNIMPLEMENTED luaL_error(L, LUA_QL("mobj_t") " field " LUA_QS " is not implemented for Lua and cannot be accessed.", mobj_opt[field])
@@ -481,6 +493,24 @@ static int mobj_get(lua_State *L)
 	case mobj_dispoffset:
 		lua_pushinteger(L, mo->dispoffset);
 		break;
+	case mobj_tid:
+		lua_pushinteger(L, mo->tid);
+		break;
+	case mobj_args:
+		LUA_PushUserdata(L, mo->thing_args, META_THINGARGS);
+		break;
+	case mobj_stringargs:
+		LUA_PushUserdata(L, mo->thing_stringargs, META_THINGSTRINGARGS);
+		break;
+	case mobj_special:
+		lua_pushinteger(L, mo->special);
+		break;
+	case mobj_specialargs:
+		LUA_PushUserdata(L, mo->script_args, META_THINGSPECIALARGS);
+		break;
+	case mobj_specialstringargs:
+		LUA_PushUserdata(L, mo->script_stringargs, META_THINGSPECIALSTRINGARGS);
+		break;
 	default: // extra custom variables in Lua memory
 		lua_getfield(L, LUA_REGISTRYINDEX, LREG_EXTVARS);
 		I_Assert(lua_istable(L, -1));
@@ -876,6 +906,17 @@ static int mobj_set(lua_State *L)
 	case mobj_dispoffset:
 		mo->dispoffset = luaL_checkinteger(L, 3);
 		break;
+	case mobj_tid:
+		P_SetThingTID(mo, luaL_checkinteger(L, 3));
+		break;
+	case mobj_special:
+		mo->special = luaL_checkinteger(L, 3);
+		break;
+	case mobj_args:
+	case mobj_stringargs:
+	case mobj_specialargs:
+	case mobj_specialstringargs:
+		return NOSET;
 	default:
 		lua_getfield(L, LUA_REGISTRYINDEX, LREG_EXTVARS);
 		I_Assert(lua_istable(L, -1));
@@ -909,16 +950,16 @@ static int thingargs_get(lua_State *L)
 {
 	INT32 *args = *((INT32**)luaL_checkudata(L, 1, META_THINGARGS));
 	int i = luaL_checkinteger(L, 2);
-	if (i < 0 || i >= NUMMAPTHINGARGS)
+	if (i < 0 || i >= NUM_MAPTHING_ARGS)
 		return luaL_error(L, LUA_QL("mapthing_t.args") " index cannot be %d", i);
 	lua_pushinteger(L, args[i]);
 	return 1;
 }
 
-// #args -> NUMMAPTHINGARGS
+// #args -> NUM_MAPTHING_ARGS
 static int thingargs_len(lua_State* L)
 {
-	lua_pushinteger(L, NUMMAPTHINGARGS);
+	lua_pushinteger(L, NUM_MAPTHING_ARGS);
 	return 1;
 }
 
@@ -927,16 +968,52 @@ static int thingstringargs_get(lua_State *L)
 {
 	char **stringargs = *((char***)luaL_checkudata(L, 1, META_THINGSTRINGARGS));
 	int i = luaL_checkinteger(L, 2);
-	if (i < 0 || i >= NUMMAPTHINGSTRINGARGS)
+	if (i < 0 || i >= NUM_MAPTHING_STRINGARGS)
 		return luaL_error(L, LUA_QL("mapthing_t.stringargs") " index cannot be %d", i);
 	lua_pushstring(L, stringargs[i]);
 	return 1;
 }
 
-// #stringargs -> NUMMAPTHINGSTRINGARGS
+// #stringargs -> NUM_MAPTHING_STRINGARGS
 static int thingstringargs_len(lua_State *L)
 {
-	lua_pushinteger(L, NUMMAPTHINGSTRINGARGS);
+	lua_pushinteger(L, NUM_MAPTHING_STRINGARGS);
+	return 1;
+}
+
+// args, i -> args[i]
+static int thingspecialargs_get(lua_State *L)
+{
+	INT32 *args = *((INT32**)luaL_checkudata(L, 1, META_THINGSPECIALARGS));
+	int i = luaL_checkinteger(L, 2);
+	if (i < 0 || i >= NUM_SCRIPT_ARGS)
+		return luaL_error(L, LUA_QL("mapthing_t.specialargs") " index cannot be %d", i);
+	lua_pushinteger(L, args[i]);
+	return 1;
+}
+
+// #args -> NUM_SCRIPT_ARGS
+static int thingspecialargs_len(lua_State* L)
+{
+	lua_pushinteger(L, NUM_SCRIPT_ARGS);
+	return 1;
+}
+
+// stringargs, i -> stringargs[i]
+static int thingspecialstringargs_get(lua_State *L)
+{
+	char **stringargs = *((char***)luaL_checkudata(L, 1, META_THINGSPECIALSTRINGARGS));
+	int i = luaL_checkinteger(L, 2);
+	if (i < 0 || i >= NUM_SCRIPT_STRINGARGS)
+		return luaL_error(L, LUA_QL("mapthing_t.specialstringargs") " index cannot be %d", i);
+	lua_pushstring(L, stringargs[i]);
+	return 1;
+}
+
+// #stringargs -> NUM_SCRIPT_STRINGARGS
+static int thingspecialstringargs_len(lua_State *L)
+{
+	lua_pushinteger(L, NUM_SCRIPT_STRINGARGS);
 	return 1;
 }
 
@@ -958,6 +1035,9 @@ enum mapthing_e {
 	mapthing_taglist,
 	mapthing_args,
 	mapthing_stringargs,
+	mapthing_special,
+	mapthing_specialargs,
+	mapthing_specialstringargs,
 	mapthing_mobj,
 };
 
@@ -979,6 +1059,9 @@ const char *const mapthing_opt[] = {
 	"taglist",
 	"args",
 	"stringargs",
+	"special",
+	"specialargs",
+	"specialstringargs",
 	"mobj",
 	NULL,
 };
@@ -1057,6 +1140,15 @@ static int mapthing_get(lua_State *L)
 		case mapthing_stringargs:
 			LUA_PushUserdata(L, mt->stringargs, META_THINGSTRINGARGS);
 			break;
+		case mapthing_special:
+			lua_pushinteger(L, mt->special);
+			break;
+		case mapthing_specialargs:
+			LUA_PushUserdata(L, mt->script_args, META_THINGSPECIALARGS);
+			break;
+		case mapthing_specialstringargs:
+			LUA_PushUserdata(L, mt->script_stringargs, META_THINGSPECIALSTRINGARGS);
+			break;
 		case mapthing_mobj:
 			LUA_PushUserdata(L, mt->mobj, META_MOBJ);
 			break;
@@ -1135,6 +1227,17 @@ static int mapthing_set(lua_State *L)
 			break;
 		case mapthing_taglist:
 			return LUA_ErrSetDirectly(L, "mapthing_t", "taglist");
+		case mapthing_special:
+			mt->special = (INT16)luaL_checkinteger(L, 3);
+			break;
+		case mapthing_args:
+			return LUA_ErrSetDirectly(L, "mapthing_t", "args");
+		case mapthing_stringargs:
+			return LUA_ErrSetDirectly(L, "mapthing_t", "stringargs");
+		case mapthing_specialargs:
+			return LUA_ErrSetDirectly(L, "mapthing_t", "specialargs");
+		case mapthing_specialstringargs:
+			return LUA_ErrSetDirectly(L, "mapthing_t", "specialstringargs");
 		case mapthing_mobj:
 			mt->mobj = *((mobj_t **)luaL_checkudata(L, 3, META_MOBJ));
 			break;
@@ -1197,6 +1300,8 @@ int LUA_MobjLib(lua_State *L)
 	LUA_RegisterUserdataMetatable(L, META_MOBJ, mobj_get, mobj_set, NULL);
 	LUA_RegisterUserdataMetatable(L, META_THINGARGS, thingargs_get, NULL, thingargs_len);
 	LUA_RegisterUserdataMetatable(L, META_THINGSTRINGARGS, thingstringargs_get, NULL, thingstringargs_len);
+	LUA_RegisterUserdataMetatable(L, META_THINGSPECIALARGS, thingspecialargs_get, NULL, thingspecialargs_len);
+	LUA_RegisterUserdataMetatable(L, META_THINGSPECIALSTRINGARGS, thingspecialstringargs_get, NULL, thingspecialstringargs_len);
 	LUA_RegisterUserdataMetatable(L, META_MAPTHING, mapthing_get, mapthing_set, mapthing_num);
 
 	mobj_fields_ref = Lua_CreateFieldTable(L, mobj_opt);
diff --git a/src/lua_script.c b/src/lua_script.c
index 057899555480383611652c552e41bf41398b0e2b..fee2cf9eea2875bbc57dd536aeaeeb5982280469 100644
--- a/src/lua_script.c
+++ b/src/lua_script.c
@@ -1001,6 +1001,8 @@ void LUA_InvalidateMapthings(void)
 		LUA_InvalidateUserdata(&mapthings[i].tags);
 		LUA_InvalidateUserdata(mapthings[i].args);
 		LUA_InvalidateUserdata(mapthings[i].stringargs);
+		LUA_InvalidateUserdata(mapthings[i].script_args);
+		LUA_InvalidateUserdata(mapthings[i].script_stringargs);
 	}
 }
 
diff --git a/src/m_cheat.c b/src/m_cheat.c
index 07f10071711025f93f91d7b7ea2d9d65f3d5ad1d..2e87e5ebca8e3fb7aee5d0f69fae0d0fc5b26b45 100644
--- a/src/m_cheat.c
+++ b/src/m_cheat.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -1105,9 +1105,6 @@ static mapthing_t *OP_CreateNewMapThing(player_t *player, UINT16 type, boolean c
 	mt->scale = player->mo->scale;
 	mt->spritexscale = player->mo->spritexscale;
 	mt->spriteyscale = player->mo->spriteyscale;
-	memset(mt->args, 0, NUMMAPTHINGARGS*sizeof(*mt->args));
-	memset(mt->stringargs, 0x00, NUMMAPTHINGSTRINGARGS*sizeof(*mt->stringargs));
-	mt->pitch = mt->roll = 0;
 
 	// Ignore offsets
 	if (mt->type == MT_EMBLEM)
diff --git a/src/m_cond.c b/src/m_cond.c
index 5a5913297157e24ada893ca5fad77a4d38ffd3e3..e6edde3c6f5f38bc3c2623d36519dbf714ccfdfd 100644
--- a/src/m_cond.c
+++ b/src/m_cond.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 2012-2016 by Matthew "Kaito Sinclaire" Walsh.
-// Copyright (C) 2012-2023 by Sonic Team Junior.
+// Copyright (C) 2012-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -443,6 +443,22 @@ UINT8 M_CompletionEmblems(gamedata_t *data) // Bah! Duplication sucks, but it's
 // -------------------
 // Quick unlock checks
 // -------------------
+boolean M_CheckNetUnlockByID(UINT16 unlockid)
+{
+	if (unlockid >= MAXUNLOCKABLES
+		|| !unlockables[unlockid].conditionset)
+	{
+		return true; // default permit
+	}
+
+	if (netgame)
+	{
+		return serverGamedata->unlocked[unlockid];
+	}
+
+	return clientGamedata->unlocked[unlockid];
+}
+
 UINT8 M_AnySecretUnlocked(gamedata_t *data)
 {
 	INT32 i;
diff --git a/src/m_cond.h b/src/m_cond.h
index 2491a384c02aa5f34ba47933eba689811ac599d0..e09447d3c3246058e7cf642d00fa1eb08afa0e15 100644
--- a/src/m_cond.h
+++ b/src/m_cond.h
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 2012-2016 by Matthew "Kaito Sinclaire" Walsh.
-// Copyright (C) 2012-2023 by Sonic Team Junior.
+// Copyright (C) 2012-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -16,6 +16,10 @@
 #include "doomdef.h"
 #include "doomdata.h"
 
+#ifdef __cplusplus
+extern "C" {
+#endif
+
 // --------
 // Typedefs
 // --------
@@ -249,6 +253,7 @@ UINT8 M_CompletionEmblems(gamedata_t *data);
 void M_SilentUpdateSkinAvailabilites(void);
 
 // Checking unlockable status
+boolean M_CheckNetUnlockByID(UINT16 unlockid);
 UINT8 M_AnySecretUnlocked(gamedata_t *data);
 UINT8 M_SecretUnlocked(INT32 type, gamedata_t *data);
 UINT8 M_MapLocked(INT32 mapnum, gamedata_t *data);
@@ -275,4 +280,8 @@ INT32 M_EmblemSkinNum(emblem_t *emblem);
 
 #define M_Achieved(a, data) ((a) >= MAXCONDITIONSETS || data->achieved[a])
 
+#ifdef __cplusplus
+} // extern "C"
+#endif
+
 #endif
diff --git a/src/m_misc.h b/src/m_misc.h
index 04ac66ca65e1ff92d44172b12e1186cdaa04b648..875e10c6d3508df15f48072f111ac2065c38a59e 100644
--- a/src/m_misc.h
+++ b/src/m_misc.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -21,6 +21,10 @@
 #include "d_event.h" // Screenshot responder
 #include "command.h"
 
+#ifdef __cplusplus
+extern "C" {
+#endif
+
 typedef enum {
 	MM_OFF = 0,
 	MM_APNG,
@@ -124,4 +128,8 @@ int M_RoundUp(double number);
 #include "w_wad.h"
 extern char configfile[MAX_WADPATH];
 
+#ifdef __cplusplus
+} // extern "C"
+#endif
+
 #endif
diff --git a/src/m_perfstats.c b/src/m_perfstats.c
index b9948bdc0c3284a3473e7e6e8b95b2e3bd8fb568..3a18376a82970fa48274e0c595cb7e950d52da72 100644
--- a/src/m_perfstats.c
+++ b/src/m_perfstats.c
@@ -1,6 +1,6 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
-// Copyright (C) 2020-2023 by Sonic Team Junior.
+// Copyright (C) 2020-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -71,6 +71,8 @@ ps_metric_t ps_lua_postthinkframe_time = {0};
 
 ps_metric_t ps_lua_mobjhooks = {0};
 
+ps_metric_t ps_acs_time = {0};
+
 ps_metric_t ps_otherlogictime = {0};
 
 // Columns for perfstats pages.
@@ -163,6 +165,7 @@ perfstatrow_t gamelogic_rows[] = {
 	{" lprethinkf", " LUAh_PreThinkFrame:", &ps_lua_prethinkframe_time, PS_TIME|PS_LEVEL},
 	{" lthinkf", " LUAh_ThinkFrame:", &ps_lua_thinkframe_time, PS_TIME|PS_LEVEL},
 	{" lpostthinkf", " LUAh_PostThinkFrame:", &ps_lua_postthinkframe_time, PS_TIME|PS_LEVEL},
+	{" acstick", " ACS_Tick:", &ps_acs_time, PS_TIME|PS_LEVEL},
 	{" other  ", " Other:          ", &ps_otherlogictime, PS_TIME|PS_LEVEL},
 	{0}
 };
@@ -629,7 +632,8 @@ void PS_UpdateTickStats(void)
 				ps_thinkertime.value.p -
 				ps_lua_prethinkframe_time.value.p -
 				ps_lua_thinkframe_time.value.p -
-				ps_lua_postthinkframe_time.value.p;
+				ps_lua_postthinkframe_time.value.p -
+				ps_acs_time.value.p;
 
 			PS_CountThinkers();
 		}
diff --git a/src/m_perfstats.h b/src/m_perfstats.h
index 592ab31d2d62adac2704b740e40f5476158e38c6..37d1d218615e5c2a4de0ab8a44b00a4536e81599 100644
--- a/src/m_perfstats.h
+++ b/src/m_perfstats.h
@@ -1,6 +1,6 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
-// Copyright (C) 2020-2023 by Sonic Team Junior.
+// Copyright (C) 2020-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -48,6 +48,8 @@ extern ps_metric_t ps_lua_thinkframe_time;
 extern ps_metric_t ps_lua_postthinkframe_time;
 extern ps_metric_t ps_lua_mobjhooks;
 
+extern ps_metric_t ps_acs_time;
+
 extern ps_metric_t ps_otherlogictime;
 
 void PS_SetPreThinkFrameHookInfo(int index, precise_t time_taken, char* short_src);
diff --git a/src/m_random.h b/src/m_random.h
index a7c07a46b5e2c951548d67f409287e80345c20dd..274ee1315aa90d776b77b0c1069e243c402c171b 100644
--- a/src/m_random.h
+++ b/src/m_random.h
@@ -4,7 +4,7 @@
 // Copyright (C) 1998-2000 by DooM Legacy Team.
 // Copyright (C) 2012-2016 by Matthew "Kaito Sinclaire" Walsh.
 // Copyright (C) 2022-2023 by tertu marybig.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -19,8 +19,11 @@
 #include "doomtype.h"
 #include "m_fixed.h"
 
-//#define DEBUGRANDOM
+#ifdef __cplusplus
+extern "C" {
+#endif
 
+//#define DEBUGRANDOM
 
 // M_Random functions pull random numbers of various types that aren't network synced.
 // P_Random functions pulls random bytes from a PRNG that is network synced.
@@ -75,4 +78,8 @@ void P_SetRandSeed(UINT32 seed);
 #endif
 UINT32 M_RandomizedSeed(void);
 
+#ifdef __cplusplus
+} // extern "C"
+#endif
+
 #endif
diff --git a/src/netcode/gamestate.c b/src/netcode/gamestate.c
index f36347c6d88e94a444fb186937d2672cfe3a0df9..afad8a9a66c1a08a97f9cc3ef82344b6d78386ee 100644
--- a/src/netcode/gamestate.c
+++ b/src/netcode/gamestate.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -67,7 +67,10 @@ void SV_SendSaveGame(INT32 node, boolean resending)
 	}
 
 	// Leave room for the uncompressed length.
-	save_p = savebuffer + sizeof(UINT32);
+	save_length = SAVEGAMESIZE;
+	save_start = savebuffer;
+	save_end = savebuffer + save_length;
+	save_p = save_start + sizeof(UINT32);
 
 	P_SaveNetGame(resending);
 
@@ -75,7 +78,6 @@ void SV_SendSaveGame(INT32 node, boolean resending)
 	if (length > SAVEGAMESIZE)
 	{
 		free(savebuffer);
-		save_p = NULL;
 		I_Error("Savegame buffer overrun");
 	}
 
@@ -112,7 +114,8 @@ void SV_SendSaveGame(INT32 node, boolean resending)
 	}
 
 	AddRamToSendQueue(node, buffertosend, length, SF_RAM, 0);
-	save_p = NULL;
+	save_p = save_start = save_end = NULL;
+	save_length = 0;
 
 	// Remember when we started sending the savegame so we can handle timeouts
 	netnodes[node].sendingsavegame = true;
@@ -184,7 +187,10 @@ void CL_LoadReceivedSavegame(boolean reloading)
 		return;
 	}
 
-	save_p = savebuffer;
+	save_length = length;
+	save_start = savebuffer;
+	save_end = save_start + save_length;
+	save_p = save_start;
 
 	// Decompress saved game if necessary.
 	decompressedlen = READUINT32(save_p);
@@ -193,7 +199,11 @@ void CL_LoadReceivedSavegame(boolean reloading)
 		UINT8 *decompressedbuffer = Z_Malloc(decompressedlen, PU_STATIC, NULL);
 		lzf_decompress(save_p, length - sizeof(UINT32), decompressedbuffer, decompressedlen);
 		Z_Free(savebuffer);
-		save_p = savebuffer = decompressedbuffer;
+
+		save_length = decompressedlen;
+		save_start = decompressedbuffer;
+		save_end = save_start + save_length;
+		save_p = savebuffer = save_start;
 	}
 
 	paused = false;
@@ -220,7 +230,8 @@ void CL_LoadReceivedSavegame(boolean reloading)
 
 	// done
 	Z_Free(savebuffer);
-	save_p = NULL;
+	save_p = save_start = save_end = NULL;
+	save_length = 0;
 	if (unlink(tmpsave) == -1)
 		CONS_Alert(CONS_ERROR, M_GetText("Can't delete %s\n"), tmpsave);
 	consistancy[gametic%BACKUPTICS] = Consistancy();
diff --git a/src/netcode/net_command.h b/src/netcode/net_command.h
index a0c46f3a2ffb26156708bfe3fbb3244b5fbc48ec..783e63efaaacf0c5d0dc1f37e139779d96b08b51 100644
--- a/src/netcode/net_command.h
+++ b/src/netcode/net_command.h
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2022 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -14,8 +14,13 @@
 #define __D_NET_COMMAND__
 
 #include "d_clisrv.h"
+
 #include "../doomtype.h"
 
+#ifdef __cplusplus
+extern "C" {
+#endif
+
 // Must be a power of two
 #define TEXTCMD_HASH_SIZE 4
 
@@ -63,4 +68,8 @@ void CL_SendNetCommands(void);
 void SendKick(UINT8 playernum, UINT8 msg);
 void SendKicksForNode(SINT8 node, UINT8 msg);
 
+#ifdef __cplusplus
+} // extern "C"
+#endif
+
 #endif
diff --git a/src/p_enemy.c b/src/p_enemy.c
index 4990db6fd50a2f96102056984b4d02e3c0205ddb..46225572429dd9153eb9e7ff71345e813e260e44 100644
--- a/src/p_enemy.c
+++ b/src/p_enemy.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -8651,7 +8651,7 @@ void A_LinedefExecuteFromArg(mobj_t *actor)
 	if (!actor->spawnpoint)
 		return;
 
-	if (locvar1 < 0 || locvar1 > NUMMAPTHINGARGS)
+	if (locvar1 < 0 || locvar1 > NUM_MAPTHING_ARGS)
 	{
 		CONS_Debug(DBG_GAMELOGIC, "A_LinedefExecuteFromArg: Invalid mapthing arg %d\n", locvar1);
 		return;
diff --git a/src/p_inter.c b/src/p_inter.c
index e73cd1fce675ee17721e6236e7dc764ede8e9d69..f8923c7a111a6a6b4525eca9d191bec31b6ef5d3 100644
--- a/src/p_inter.c
+++ b/src/p_inter.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -22,6 +22,7 @@
 #include "st_stuff.h"
 #include "hu_stuff.h"
 #include "lua_hook.h"
+#include "acs/interface.h"
 #include "m_cond.h" // unlockables, emblems, etc
 #include "p_setup.h"
 #include "m_cheat.h" // objectplace
@@ -2543,6 +2544,8 @@ void P_KillMobj(mobj_t *target, mobj_t *inflictor, mobj_t *source, UINT8 damaget
 	if (LUA_HookMobjDeath(target, inflictor, source, damagetype) || P_MobjWasRemoved(target))
 		return;
 
+	P_ActivateThingSpecial(target, source);
+
 	// Let EVERYONE know what happened to a player! 01-29-2002 Tails
 	if (target->player && !target->player->spectator)
 	{
@@ -2780,6 +2783,8 @@ void P_KillMobj(mobj_t *target, mobj_t *inflictor, mobj_t *source, UINT8 damaget
 				}
 			}
 		}
+
+		ACS_RunPlayerDeathScript(target->player);
 	}
 
 	if (source && target && target->player && source->player)
diff --git a/src/p_local.h b/src/p_local.h
index 249c3cd4b6de5248140c00e2dfc2332081a5d00e..54c9fabb8ae1da2cb291b620f7231cace70f0fe7 100644
--- a/src/p_local.h
+++ b/src/p_local.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -23,6 +23,10 @@
 #include "r_defs.h"
 #include "p_maputl.h"
 
+#ifdef __cplusplus
+extern "C" {
+#endif
+
 #define FLOATSPEED (FRACUNIT*4)
 
 // Maximum player score.
@@ -74,6 +78,7 @@ extern thinker_t thlist[];
 extern mobj_t *mobjcache;
 
 void P_InitThinkers(void);
+void P_InvalidateThinkersWithoutInit(void);
 void P_AddThinker(const thinklistnum_t n, thinker_t *thinker);
 void P_RemoveThinker(thinker_t *thinker);
 
@@ -152,6 +157,7 @@ UINT16 P_GetPlayerColor(player_t *player);
 boolean P_IsObjectInGoop(mobj_t *mo);
 boolean P_IsObjectOnGround(mobj_t *mo);
 boolean P_InSpaceSector(mobj_t *mo);
+#define P_IsObjectFlipped(o) (((o)->eflags & MFE_VERTICALFLIP) == MFE_VERTICALFLIP)
 boolean P_InQuicksand(mobj_t *mo);
 boolean P_InJumpFlipSector(mobj_t *mo);
 boolean P_PlayerHitFloor(player_t *player, boolean dorollstuff);
@@ -209,6 +215,8 @@ void P_DoSpinDashDust(player_t *player);
 #define P_AnalogMove(player) (P_ControlStyle(player) == CS_LMAOGALOG)
 boolean P_TransferToNextMare(player_t *player);
 UINT8 P_FindLowestMare(void);
+UINT8 P_FindLowestLap(void);
+UINT8 P_FindHighestLap(void);
 void P_FindEmerald(void);
 void P_TransferToAxis(player_t *player, INT32 axisnum);
 boolean P_PlayerMoving(INT32 pnum);
@@ -553,5 +561,18 @@ void P_DoSuperDetransformation(player_t *player);
 void P_ExplodeMissile(mobj_t *mo);
 void P_CheckGravity(mobj_t *mo, boolean affect);
 void P_SetPitchRollFromSlope(mobj_t *mo, pslope_t *slope);
+fixed_t P_GetMobjHead(mobj_t *mo);
+fixed_t P_GetMobjFeet(mobj_t *mo);
+
+void P_InitTIDHash(void);
+void P_SetThingTID(mobj_t *mo, mtag_t tid);
+void P_RemoveThingTID(mobj_t *mo);
+mobj_t *P_FindMobjFromTID(mtag_t tid, mobj_t *i, mobj_t *activator);
+
+void P_DeleteMobjStringArgs(mobj_t *mobj);
+
+#ifdef __cplusplus
+} // extern "C"
+#endif
 
 #endif // __P_LOCAL__
diff --git a/src/p_map.c b/src/p_map.c
index f97ddfa3cd8aa3fa9f7e9dbb3e6d76b041aca164..8d98f8b6b3c45583df1e08fa10558dfc322c67f5 100644
--- a/src/p_map.c
+++ b/src/p_map.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -64,6 +64,19 @@ line_t *ceilingline;
 // that is, for any line which is 'solid'
 line_t *blockingline;
 
+// Mostly re-ported from DOOM Legacy
+// Keep track of special lines as they are hit, process them when the move is valid
+static size_t *spechit = NULL;
+static size_t spechit_max = 0U;
+static size_t numspechit = 0U;
+
+// Need a intermediate buffer for P_TryMove because it performs multiple moves
+// the lines put into spechit will be moved into here after each checkposition,
+// then and duplicates will be removed before processing
+static size_t *spechitint = NULL;
+static size_t spechitint_max = 0U;
+static size_t numspechitint = 0U;
+
 msecnode_t *sector_list = NULL;
 mprecipsecnode_t *precipsector_list = NULL;
 camera_t *mapcampointer;
@@ -77,6 +90,11 @@ camera_t *mapcampointer;
 //
 static boolean P_TeleportMove(mobj_t *thing, fixed_t x, fixed_t y, fixed_t z)
 {
+	boolean startingonground = P_IsObjectOnGround(thing);
+	sector_t *oldsector = thing->subsector->sector;
+
+	numspechit = 0U;
+
 	// the move is ok,
 	// so link the thing into its new position
 	P_UnsetThingPosition(thing);
@@ -104,6 +122,8 @@ static boolean P_TeleportMove(mobj_t *thing, fixed_t x, fixed_t y, fixed_t z)
 	thing->floorrover = tmfloorrover;
 	thing->ceilingrover = tmceilingrover;
 
+	P_CheckSectorTransitionalEffects(thing, oldsector, startingonground);
+
 	return true;
 }
 
@@ -135,6 +155,92 @@ boolean P_MoveOrigin(mobj_t *thing, fixed_t x, fixed_t y, fixed_t z)
 //                       MOVEMENT ITERATOR FUNCTIONS
 // =========================================================================
 
+// For our intermediate buffer, remove any duplicate entries by adding each one to
+// a temprary buffer if it's not already in there, copy the temporary buffer back over the intermediate afterwards
+static void spechitint_removedups(void)
+{
+	// Only needs to be run if there's more than 1 line crossed
+	if (numspechitint > 1U)
+	{
+		boolean valueintemp = false;
+		size_t i = 0U, j = 0U;
+		size_t numspechittemp = 0U;
+		size_t *spechittemp = Z_Calloc(numspechitint * sizeof(size_t), PU_STATIC, NULL);
+
+		// Fill the hashtable
+		for (i = 0U; i < numspechitint; i++)
+		{
+			valueintemp = false;
+			for (j = 0; j < numspechittemp; j++)
+			{
+				if (spechitint[i] == spechittemp[j])
+				{
+					valueintemp = true;
+					break;
+				}
+			}
+
+			if (!valueintemp)
+			{
+				spechittemp[numspechittemp] = spechitint[i];
+				numspechittemp++;
+			}
+		}
+
+		// The hash table now IS the result we want to send back
+		// easiest way to handle this is a memcpy
+		if (numspechittemp != numspechitint)
+		{
+			memcpy(spechitint, spechittemp, numspechittemp * sizeof(size_t));
+			numspechitint = numspechittemp;
+		}
+
+		Z_Free(spechittemp);
+	}
+}
+
+// copy the contents of spechit into the end of spechitint
+static void spechitint_copyinto(void)
+{
+	if (numspechit > 0U)
+	{
+		if (numspechitint + numspechit >= spechitint_max)
+		{
+			spechitint_max = spechitint_max + numspechit;
+			spechitint = Z_Realloc(spechitint, spechitint_max * sizeof(size_t), PU_STATIC, NULL);
+		}
+
+		memcpy(&spechitint[numspechitint], spechit, numspechit * sizeof(size_t));
+		numspechitint += numspechit;
+	}
+}
+
+static void add_spechit(line_t *ld)
+{
+	if (numspechit >= spechit_max)
+	{
+		spechit_max = spechit_max ? spechit_max * 2U : 16U;
+		spechit = Z_Realloc(spechit, spechit_max * sizeof(size_t), PU_STATIC, NULL);
+	}
+
+	spechit[numspechit] = ld - lines;
+	numspechit++;
+}
+
+static boolean P_SpecialIsLinedefCrossType(line_t *ld)
+{
+	boolean linedefcrossspecial = false;
+
+	// Take anything with any cross type for now,
+	// we'll have to filter it down later...
+	if (ld->activation & (SPAC_CROSS | SPAC_CROSSMONSTER | SPAC_CROSSMISSILE))
+	{
+		linedefcrossspecial = P_CanActivateSpecial(ld->special);
+	}
+
+	return linedefcrossspecial;
+}
+
 // P_DoSpring
 //
 // MF_SPRING does some weird, mildly hacky stuff sometimes.
@@ -2015,6 +2121,12 @@ static boolean PIT_CheckLine(line_t *ld)
 	if (lowfloor < tmdropoffz)
 		tmdropoffz = lowfloor;
 
+	// we've crossed the line
+	if (P_SpecialIsLinedefCrossType(ld))
+	{
+		add_spechit(ld);
+	}
+
 	return true;
 }
 
@@ -2682,6 +2794,9 @@ increment_move
 	fixed_t thingtop;
 	floatok = false;
 
+	// reset this to 0 at the start of each trymove call as it's only used here
+	numspechitint = 0U;
+
 	// This makes sure that there are no freezes from computing extremely small movements.
 	// Originally was MAXRADIUS/2, but that can cause some bad inconsistencies for small players.
 	radius = max(radius, thing->scale);
@@ -2721,6 +2836,9 @@ increment_move
 		if (!P_CheckPosition(thing, tryx, tryy) || P_MobjWasRemoved(thing))
 			return false; // solid wall or thing
 
+		// copy into the spechitint buffer from spechit
+		spechitint_copyinto();
+
 		if (!(thing->flags & MF_NOCLIP))
 		{
 			//All things are affected by their scale.
@@ -2860,7 +2978,10 @@ boolean P_CheckMove(mobj_t *thing, fixed_t x, fixed_t y, boolean allowdropoff)
 //
 boolean P_TryMove(mobj_t *thing, fixed_t x, fixed_t y, boolean allowdropoff)
 {
+	fixed_t oldx = thing->x;
+	fixed_t oldy = thing->y;
 	fixed_t startingonground = P_IsObjectOnGround(thing);
+	sector_t *oldsector = thing->subsector->sector;
 
 	// The move is ok!
 	if (!increment_move(thing, x, y, allowdropoff))
@@ -2936,6 +3057,29 @@ boolean P_TryMove(mobj_t *thing, fixed_t x, fixed_t y, boolean allowdropoff)
 		thing->eflags |= MFE_ONGROUND;
 
 	P_SetThingPosition(thing);
+
+	P_CheckSectorTransitionalEffects(thing, oldsector, startingonground);
+
+	// remove any duplicates that may be in spechitint
+	spechitint_removedups();
+
+	// handle any of the special lines that were crossed
+	if (!(thing->flags & (MF_NOCLIP)))
+	{
+		line_t *ld = NULL;
+		INT32 side = 0, oldside = 0;
+		while (numspechitint--)
+		{
+			ld = &lines[spechitint[numspechitint]];
+			side = P_PointOnLineSide(thing->x, thing->y, ld);
+			oldside = P_PointOnLineSide(oldx, oldy, ld);
+			if (side != oldside)
+			{
+				P_CrossSpecialLine(ld, oldside, thing);
+			}
+		}
+	}
+
 	return true;
 }
 
diff --git a/src/p_mobj.c b/src/p_mobj.c
index 9cdd2628db8cfec3ee8af83a6f96b3713879b164..2f04b17f7b7e6e65be039e4202c42ee36b1f6c24 100644
--- a/src/p_mobj.c
+++ b/src/p_mobj.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -1748,7 +1748,10 @@ void P_XYMovement(mobj_t *mo)
 		}
 		else if (P_MobjWasRemoved(mo))
 			return;
-		else if (mo->flags & MF_BOUNCE)
+
+		P_PushSpecialLine(blockingline, mo);
+
+		if (mo->flags & MF_BOUNCE)
 		{
 			P_BounceMove(mo);
 			xmove = ymove = 0;
@@ -10169,6 +10172,12 @@ void P_MobjThinker(mobj_t *mobj)
 
 	tmfloorthing = tmhitthing = NULL;
 
+	if (udmf)
+	{
+		// Check for continuous sector special actions
+		P_CheckMobjTouchingSectorActions(mobj, true, true);
+	}
+
 	// Sector flag MSF_TRIGGERLINE_MOBJ allows ANY mobj to trigger a linedef exec
 	P_CheckMobjTrigger(mobj, false);
 
@@ -11252,6 +11261,8 @@ void P_RemoveMobj(mobj_t *mobj)
 	if (mobj->spawnpoint)
 		mobj->spawnpoint->mobj = NULL;
 
+	P_RemoveThingTID(mobj);
+	P_DeleteMobjStringArgs(mobj);
 	R_RemoveMobjInterpolator(mobj);
 
 	// free block
@@ -13459,6 +13470,65 @@ static void P_SetAmbush(mapthing_t *mthing, mobj_t *mobj)
 	mobj->flags2 |= MF2_AMBUSH;
 }
 
+void P_CopyMapThingSpecialFieldsToMobj(const mapthing_t *mthing, mobj_t *mobj)
+{
+	size_t arg = SIZE_MAX;
+
+	P_SetThingTID(mobj, mthing->tid);
+
+	mobj->special = mthing->special;
+
+	for (arg = 0; arg < NUM_MAPTHING_ARGS; arg++)
+	{
+		mobj->thing_args[arg] = mthing->args[arg];
+	}
+
+	for (arg = 0; arg < NUM_MAPTHING_STRINGARGS; arg++)
+	{
+		size_t len = 0;
+
+		if (mthing->stringargs[arg])
+		{
+			len = strlen(mthing->stringargs[arg]);
+		}
+
+		if (len == 0)
+		{
+			Z_Free(mobj->thing_stringargs[arg]);
+			mobj->thing_stringargs[arg] = NULL;
+			continue;
+		}
+
+		mobj->thing_stringargs[arg] = Z_Realloc(mobj->thing_stringargs[arg], len + 1, PU_LEVEL, NULL);
+		M_Memcpy(mobj->thing_stringargs[arg], mthing->stringargs[arg], len + 1);
+	}
+
+	for (arg = 0; arg < NUM_SCRIPT_ARGS; arg++)
+	{
+		mobj->script_args[arg] = mthing->script_args[arg];
+	}
+
+	for (arg = 0; arg < NUM_SCRIPT_STRINGARGS; arg++)
+	{
+		size_t len = 0;
+
+		if (mthing->script_stringargs[arg])
+		{
+			len = strlen(mthing->script_stringargs[arg]);
+		}
+
+		if (len == 0)
+		{
+			Z_Free(mobj->script_stringargs[arg]);
+			mobj->script_stringargs[arg] = NULL;
+			continue;
+		}
+
+		mobj->script_stringargs[arg] = Z_Realloc(mobj->script_stringargs[arg], len + 1, PU_LEVEL, NULL);
+		M_Memcpy(mobj->script_stringargs[arg], mthing->script_stringargs[arg], len + 1);
+	}
+}
+
 static mobj_t *P_SpawnMobjFromMapThing(mapthing_t *mthing, fixed_t x, fixed_t y, fixed_t z, mobjtype_t i)
 {
 	mobj_t *mobj = NULL;
@@ -13476,6 +13546,8 @@ static mobj_t *P_SpawnMobjFromMapThing(mapthing_t *mthing, fixed_t x, fixed_t y,
 	mobj->spritexscale = mthing->spritexscale;
 	mobj->spriteyscale = mthing->spriteyscale;
 
+	P_CopyMapThingSpecialFieldsToMobj(mthing, mobj);
+
 	if (!P_SetupSpawnedMapThing(mthing, mobj, &doangle))
 		return mobj;
 
@@ -14350,3 +14422,150 @@ mobj_t *P_SpawnMobjFromMobj(mobj_t *mobj, fixed_t xofs, fixed_t yofs, fixed_t zo
 
 	return newmobj;
 }
+
+//
+// P_GetMobjHead & P_GetMobjFeet
+// Returns the top and bottom of an object, follows appearance, not physics,
+// in reverse gravity.
+//
+
+fixed_t P_GetMobjHead(mobj_t *mobj)
+{
+	return P_IsObjectFlipped(mobj) ? mobj->z : mobj->z + mobj->height;
+}
+
+fixed_t P_GetMobjFeet(mobj_t *mobj)
+{
+	/*
+	            /   |
+	           /    |
+	 /--\-----/     |
+	( ( (         ( |
+	 \--------------/
+	*/
+
+	return P_IsObjectFlipped(mobj) ? mobj->z + mobj->height : mobj->z;
+}
+
+//
+// Thing IDs / tags
+//
+// TODO: Replace this system with taglist_t instead.
+// The issue is that those require a static numbered ID
+// to determine which struct it belongs to, which mobjs
+// don't really have.
+//
+
+#define TID_HASH_CHAINS (131)
+static mobj_t *TID_Hash[TID_HASH_CHAINS];
+
+//
+// P_InitTIDHash
+// Initializes mobj tag hash array
+//
+void P_InitTIDHash(void)
+{
+	memset(TID_Hash, 0, TID_HASH_CHAINS * sizeof(mobj_t *));
+}
+
+//
+// P_SetThingTID
+// Adds a mobj to the hash array
+//
+void P_SetThingTID(mobj_t *mo, mtag_t tid)
+{
+	INT32 key = 0;
+
+	if (tid == 0)
+	{
+		if (mo->tid != 0)
+		{
+			P_RemoveThingTID(mo);
+		}
+
+		return;
+	}
+
+	mo->tid = tid;
+
+	// Insert at the head of this chain
+	key = tid % TID_HASH_CHAINS;
+
+	mo->tid_next = TID_Hash[key];
+	mo->tid_prev = &TID_Hash[key];
+	TID_Hash[key] = mo;
+
+	// Connect to any existing things in chain
+	if (mo->tid_next != NULL)
+	{
+		mo->tid_next->tid_prev = &(mo->tid_next);
+	}
+}
+
+//
+// P_RemoveThingTID
+// Removes a mobj from the hash array
+//
+void P_RemoveThingTID(mobj_t *mo)
+{
+	if (mo->tid != 0 && mo->tid_prev != NULL)
+	{
+		// Fix the gap this would leave.
+		*(mo->tid_prev) = mo->tid_next;
+
+		if (mo->tid_next != NULL)
+		{
+			mo->tid_next->tid_prev = mo->tid_prev;
+		}
+	}
+
+	// Remove TID.
+	mo->tid = 0;
+}
+
+//
+// P_FindMobjFromTID
+// Mobj tag search function.
+//
+mobj_t *P_FindMobjFromTID(mtag_t tid, mobj_t *i, mobj_t *activator)
+{
+	if (tid == 0)
+	{
+		// 0 grabs the activator, if applicable,
+		// for some ACS functions.
+
+		if (i != NULL)
+		{
+			// Don't do more than once.
+			return NULL;
+		}
+
+		return activator;
+	}
+
+	i = (i != NULL) ? i->tid_next : TID_Hash[tid % TID_HASH_CHAINS];
+
+	while (i != NULL && i->tid != tid)
+	{
+		i = i->tid_next;
+	}
+
+	return i;
+}
+
+void P_DeleteMobjStringArgs(mobj_t *mobj)
+{
+	size_t i = SIZE_MAX;
+
+	for (i = 0; i < NUM_MAPTHING_STRINGARGS; i++)
+	{
+		Z_Free(mobj->thing_stringargs[i]);
+		mobj->thing_stringargs[i] = NULL;
+	}
+
+	for (i = 0; i < NUM_SCRIPT_STRINGARGS; i++)
+	{
+		Z_Free(mobj->script_stringargs[i]);
+		mobj->script_stringargs[i] = NULL;
+	}
+}
diff --git a/src/p_mobj.h b/src/p_mobj.h
index 2f013a2f30fac7f7d117cb190fcbc5bd1db17e54..47d22b97a68e4dd9539aa3a78eccfc2d9364b9d5 100644
--- a/src/p_mobj.h
+++ b/src/p_mobj.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -28,6 +28,10 @@
 // Needs precompiled tables/data structures.
 #include "info.h"
 
+#ifdef __cplusplus
+extern "C" {
+#endif
+
 //
 // NOTES: mobj_t
 //
@@ -343,6 +347,10 @@ typedef struct mobj_s
 	UINT32 flags2; // MF2_ flags
 	UINT16 eflags; // extra flags
 
+	mtag_t tid;
+	struct mobj_s *tid_next;
+	struct mobj_s **tid_prev; // killough 8/11/98: change to ptr-to-ptr
+
 	void *skin; // overrides 'sprite' when non-NULL (for player bodies to 'remember' the skin)
 
 	// Player and mobj sprites in multiplayer modes are modified
@@ -420,6 +428,13 @@ typedef struct mobj_s
 	fixed_t shadowscale; // If this object casts a shadow, and the size relative to radius
 	INT32 dispoffset; // copy of info->dispoffset, so mobjs can be sorted independently of their type
 
+	INT32 thing_args[NUM_MAPTHING_ARGS];
+	char *thing_stringargs[NUM_MAPTHING_STRINGARGS];
+
+	INT16 special;
+	INT32 script_args[NUM_SCRIPT_ARGS];
+	char *script_stringargs[NUM_SCRIPT_STRINGARGS];
+
 	// WARNING: New fields must be added separately to savegame and Lua.
 } mobj_t;
 
@@ -512,6 +527,7 @@ fixed_t P_GetMobjSpawnHeight(const mobjtype_t mobjtype, const fixed_t x, const f
 fixed_t P_GetMapThingSpawnHeight(const mobjtype_t mobjtype, const mapthing_t* mthing, const fixed_t x, const fixed_t y);
 
 mobj_t *P_SpawnMapThing(mapthing_t *mthing);
+void P_CopyMapThingSpecialFieldsToMobj(const mapthing_t *mthing, mobj_t *mobj);
 void P_SpawnHoop(mapthing_t *mthing);
 void P_SetBonusTime(mobj_t *mobj);
 void P_SpawnItemPattern(mapthing_t *mthing, boolean bonustime);
@@ -547,4 +563,9 @@ extern UINT16 emeraldspawndelay;
 extern INT32 numstarposts;
 extern UINT16 bossdisabled;
 extern boolean stoppedclock;
+
+#ifdef __cplusplus
+} // extern "C"
+#endif
+
 #endif
diff --git a/src/p_polyobj.h b/src/p_polyobj.h
index 0573f6350aec32814e2ca03fc58aaa0c0e8e4bb6..32e5827faed63f4f8d2c958c8c1cd8c466f0a2ff 100644
--- a/src/p_polyobj.h
+++ b/src/p_polyobj.h
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 2006      by James Haley
-// Copyright (C) 2006-2023 by Sonic Team Junior.
+// Copyright (C) 2006-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -18,6 +18,10 @@
 #include "p_mobj.h"
 #include "r_defs.h"
 
+#ifdef __cplusplus
+extern "C" {
+#endif
+
 //
 // Defines
 //
@@ -411,6 +415,8 @@ extern polyobj_t *PolyObjects;
 extern INT32 numPolyObjects;
 extern polymaplink_t **polyblocklinks; // polyobject blockmap
 
+#ifdef __cplusplus
+} // extern "C"
 #endif
 
-// EOF
+#endif
diff --git a/src/p_saveg.c b/src/p_saveg.c
index 5e4d6d0760441e6bc94c6815824b8b7e1ab38c80..75185a8acf87889316581ad0ed547a26193922a1 100644
--- a/src/p_saveg.c
+++ b/src/p_saveg.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -34,10 +34,14 @@
 #include "r_sky.h"
 #include "p_polyobj.h"
 #include "lua_script.h"
+#include "acs/interface.h"
 #include "p_slopes.h"
 
 savedata_t savedata;
 UINT8 *save_p;
+UINT8 *save_start;
+UINT8 *save_end;
+size_t save_length;
 
 // Block UINT32s to attempt to ensure that the correct data is
 // being sent and received
@@ -879,6 +883,35 @@ static void P_NetUnArchiveWaypoints(void)
 #define SD_GRAVITY     0x01
 #define SD_FLOORPORTAL 0x02
 #define SD_CEILPORTAL  0x04
+#define SD_ACTION      0x08
+#define SD_ARGS        0x10
+#define SD_STRINGARGS  0x20
+#define SD_ACTIVATION  0x40
+
+static boolean P_SectorArgsEqual(const sector_t *sc, const sector_t *spawnsc)
+{
+	UINT8 i;
+	for (i = 0; i < NUM_SCRIPT_ARGS; i++)
+		if (sc->args[i] != spawnsc->args[i])
+			return false;
+
+	return true;
+}
+
+static boolean P_SectorStringArgsEqual(const sector_t *sc, const sector_t *spawnsc)
+{
+	UINT8 i;
+	for (i = 0; i < NUM_SCRIPT_STRINGARGS; i++)
+	{
+		if (!sc->stringargs[i])
+			return !spawnsc->stringargs[i];
+
+		if (strcmp(sc->stringargs[i], spawnsc->stringargs[i]))
+			return false;
+	}
+
+	return true;
+}
 
 // diff1 flags
 #define LD_FLAG          0x01
@@ -893,6 +926,7 @@ static void P_NetUnArchiveWaypoints(void)
 // diff2 flags
 #define LD_EXECUTORDELAY 0x01
 #define LD_TRANSFPORTAL  0x02
+#define LD_ACTIVATION    0x04
 
 // sidedef flags
 enum
@@ -925,7 +959,7 @@ enum
 static boolean P_AreArgsEqual(const line_t *li, const line_t *spawnli)
 {
 	UINT8 i;
-	for (i = 0; i < NUMLINEARGS; i++)
+	for (i = 0; i < NUM_SCRIPT_ARGS; i++)
 		if (li->args[i] != spawnli->args[i])
 			return false;
 
@@ -935,7 +969,7 @@ static boolean P_AreArgsEqual(const line_t *li, const line_t *spawnli)
 static boolean P_AreStringArgsEqual(const line_t *li, const line_t *spawnli)
 {
 	UINT8 i;
-	for (i = 0; i < NUMLINESTRINGARGS; i++)
+	for (i = 0; i < NUM_SCRIPT_STRINGARGS; i++)
 	{
 		if (!li->stringargs[i])
 			return !spawnli->stringargs[i];
@@ -1112,6 +1146,14 @@ static void ArchiveSectors(void)
 			diff5 |= SD_FLOORPORTAL;
 		if (ss->portal_ceiling != spawnss->portal_ceiling)
 			diff5 |= SD_CEILPORTAL;
+		if (ss->action != spawnss->action)
+			diff5 |= SD_ACTION;
+		if (!P_SectorArgsEqual(ss, spawnss))
+			diff5 |= SD_ARGS;
+		if (!P_SectorStringArgsEqual(ss, spawnss))
+			diff5 |= SD_STRINGARGS;
+		if (ss->activation != spawnss->activation)
+			diff5 |= SD_ACTIVATION;
 
 		if (ss->ffloors && CheckFFloorDiff(ss))
 			diff |= SD_FFLOORS;
@@ -1210,6 +1252,33 @@ static void ArchiveSectors(void)
 				WRITEUINT32(save_p, ss->portal_floor);
 			if (diff5 & SD_CEILPORTAL)
 				WRITEUINT32(save_p, ss->portal_ceiling);
+			if (diff5 & SD_ACTION)
+				WRITEINT16(save_p, ss->action);
+			if (diff5 & SD_ARGS)
+			{
+				for (j = 0; j < NUM_SCRIPT_ARGS; j++)
+					WRITEINT32(save_p, ss->args[j]);
+			}
+			if (diff5 & SD_STRINGARGS)
+			{
+				for (j = 0; j < NUM_SCRIPT_STRINGARGS; j++)
+				{
+					size_t len, k;
+
+					if (!ss->stringargs[j])
+					{
+						WRITEINT32(save_p, 0);
+						continue;
+					}
+
+					len = strlen(ss->stringargs[j]);
+					WRITEINT32(save_p, len);
+					for (k = 0; k < len; k++)
+						WRITECHAR(save_p, ss->stringargs[j][k]);
+				}
+			}
+			if (diff5 & SD_ACTIVATION)
+				WRITEUINT32(save_p, ss->activation);
 			if (diff & SD_FFLOORS)
 				ArchiveFFloors(ss);
 		}
@@ -1347,6 +1416,35 @@ static void UnArchiveSectors(void)
 			sectors[i].portal_floor = READUINT32(save_p);
 		if (diff5 & SD_CEILPORTAL)
 			sectors[i].portal_ceiling = READUINT32(save_p);
+		if (diff5 & SD_ACTION)
+			sectors[i].action = READINT16(save_p);
+		if (diff5 & SD_ARGS)
+		{
+			for (j = 0; j < NUM_SCRIPT_ARGS; j++)
+				sectors[i].args[j] = READINT32(save_p);
+		}
+		if (diff5 & SD_STRINGARGS)
+		{
+			for (j = 0; j < NUM_SCRIPT_STRINGARGS; j++)
+			{
+				size_t len = READINT32(save_p);
+				size_t k;
+
+				if (!len)
+				{
+					Z_Free(sectors[i].stringargs[j]);
+					sectors[i].stringargs[j] = NULL;
+					continue;
+				}
+
+				sectors[i].stringargs[j] = Z_Realloc(sectors[i].stringargs[j], len + 1, PU_LEVEL, NULL);
+				for (k = 0; k < len; k++)
+					sectors[i].stringargs[j][k] = READCHAR(save_p);
+				sectors[i].stringargs[j][len] = '\0';
+			}
+		}
+		if (diff5 & SD_ACTIVATION)
+			sectors[i].activation = READUINT32(save_p);
 
 		if (diff & SD_FFLOORS)
 			UnArchiveFFloors(&sectors[i]);
@@ -1470,6 +1568,9 @@ static void ArchiveLines(void)
 		if (li->secportal != spawnli->secportal)
 			diff2 |= LD_TRANSFPORTAL;
 
+		if (li->activation != spawnli->activation)
+			diff2 |= LD_ACTIVATION;
+
 		if (li->sidenum[0] != NO_SIDEDEF)
 		{
 			side1diff = GetSideDiff(&sides[li->sidenum[0]], &spawnsides[li->sidenum[0]]);
@@ -1501,13 +1602,13 @@ static void ArchiveLines(void)
 			if (diff & LD_ARGS)
 			{
 				UINT8 j;
-				for (j = 0; j < NUMLINEARGS; j++)
+				for (j = 0; j < NUM_SCRIPT_ARGS; j++)
 					WRITEINT32(save_p, li->args[j]);
 			}
 			if (diff & LD_STRINGARGS)
 			{
 				UINT8 j;
-				for (j = 0; j < NUMLINESTRINGARGS; j++)
+				for (j = 0; j < NUM_SCRIPT_STRINGARGS; j++)
 				{
 					size_t len, k;
 
@@ -1531,6 +1632,8 @@ static void ArchiveLines(void)
 				WRITEINT32(save_p, li->executordelay);
 			if (diff2 & LD_TRANSFPORTAL)
 				WRITEUINT32(save_p, li->secportal);
+			if (diff2 & LD_ACTIVATION)
+				WRITEUINT32(save_p, li->activation);
 		}
 	}
 	WRITEUINT32(save_p, 0xffffffff);
@@ -1609,13 +1712,13 @@ static void UnArchiveLines(void)
 		if (diff & LD_ARGS)
 		{
 			UINT8 j;
-			for (j = 0; j < NUMLINEARGS; j++)
+			for (j = 0; j < NUM_SCRIPT_ARGS; j++)
 				li->args[j] = READINT32(save_p);
 		}
 		if (diff & LD_STRINGARGS)
 		{
 			UINT8 j;
-			for (j = 0; j < NUMLINESTRINGARGS; j++)
+			for (j = 0; j < NUM_SCRIPT_STRINGARGS; j++)
 			{
 				size_t len = READINT32(save_p);
 				size_t k;
@@ -1641,6 +1744,8 @@ static void UnArchiveLines(void)
 			li->executordelay = READINT32(save_p);
 		if (diff2 & LD_TRANSFPORTAL)
 			li->secportal = READUINT32(save_p);
+		if (diff2 & LD_ACTIVATION)
+			li->activation = READUINT32(save_p);
 	}
 }
 
@@ -1682,6 +1787,53 @@ static void P_NetUnArchiveWorld(void)
 // Thinkers
 //
 
+static boolean P_ThingArgsEqual(const mobj_t *mobj, const mapthing_t *mapthing)
+{
+	UINT8 i;
+	for (i = 0; i < NUM_MAPTHING_ARGS; i++)
+		if (mobj->thing_args[i] != mapthing->args[i])
+			return false;
+
+	return true;
+}
+
+static boolean P_ThingStringArgsEqual(const mobj_t *mobj, const mapthing_t *mapthing)
+{
+	UINT8 i;
+	for (i = 0; i < NUM_MAPTHING_STRINGARGS; i++)
+	{
+		if (!mobj->thing_stringargs[i])
+			return !mapthing->stringargs[i];
+
+		if (strcmp(mobj->thing_stringargs[i], mapthing->stringargs[i]))
+			return false;
+	}
+
+	return true;
+}
+
+static boolean P_ThingScriptEqual(const mobj_t *mobj, const mapthing_t *mapthing)
+{
+	UINT8 i;
+	if (mobj->special != mapthing->special)
+		return false;
+
+	for (i = 0; i < NUM_SCRIPT_ARGS; i++)
+		if (mobj->script_args[i] != mapthing->script_args[i])
+			return false;
+
+	for (i = 0; i < NUM_SCRIPT_STRINGARGS; i++)
+	{
+		if (!mobj->script_stringargs[i])
+			return !mapthing->script_stringargs[i];
+
+		if (strcmp(mobj->script_stringargs[i], mapthing->script_stringargs[i]))
+			return false;
+	}
+
+	return true;
+}
+
 typedef enum
 {
 	MD_SPAWNPOINT  = 1,
@@ -1746,7 +1898,11 @@ typedef enum
 	MD2_DISPOFFSET          = 1<<23,
 	MD2_DRAWONLYFORPLAYER   = 1<<24,
 	MD2_DONTDRAWFORVIEWMOBJ = 1<<25,
-	MD2_TRANSLATION         = 1<<26
+	MD2_TRANSLATION         = 1<<26,
+	MD2_ARGS                = 1<<27,
+	MD2_STRINGARGS          = 1<<28,
+	MD2_TID                 = 1<<29,
+	MD2_SPECIAL             = 1<<30
 } mobj_diff2_t;
 
 typedef enum
@@ -1843,6 +1999,8 @@ static void SaveMobjThinker(const thinker_t *th, const UINT8 type)
 	if (mobj->type == MT_HOOPCENTER && mobj->threshold == 4242)
 		return;
 
+	diff2 = 0;
+
 	if (mobj->spawnpoint && mobj->info->doomednum != -1)
 	{
 		// spawnpoint is not modified but we must save it since it is an identifier
@@ -1857,11 +2015,61 @@ static void SaveMobjThinker(const thinker_t *th, const UINT8 type)
 
 		if (mobj->info->doomednum != mobj->spawnpoint->type)
 			diff |= MD_TYPE;
+
+		if (!P_ThingArgsEqual(mobj, mobj->spawnpoint))
+			diff2 |= MD2_ARGS;
+
+		if (!P_ThingStringArgsEqual(mobj, mobj->spawnpoint))
+			diff2 |= MD2_STRINGARGS;
+
+		if (!P_ThingScriptEqual(mobj, mobj->spawnpoint))
+			diff2 |= MD2_SPECIAL;
 	}
 	else
+	{
 		diff = MD_POS | MD_TYPE; // not a map spawned thing so make it from scratch
 
-	diff2 = 0;
+		for (unsigned j = 0; j < NUM_MAPTHING_ARGS; j++)
+		{
+			if (mobj->thing_args[j] != 0)
+			{
+				diff2 |= MD2_ARGS;
+				break;
+			}
+		}
+
+		for (unsigned j = 0; j < NUM_MAPTHING_STRINGARGS; j++)
+		{
+			if (mobj->thing_stringargs[j] != NULL)
+			{
+				diff2 |= MD2_STRINGARGS;
+				break;
+			}
+		}
+
+		if (mobj->special != 0)
+		{
+			diff2 |= MD2_SPECIAL;
+		}
+
+		for (unsigned j = 0; j < NUM_SCRIPT_ARGS; j++)
+		{
+			if (mobj->script_args[j] != 0)
+			{
+				diff2 |= MD2_SPECIAL;
+				break;
+			}
+		}
+
+		for (unsigned j = 0; j < NUM_SCRIPT_STRINGARGS; j++)
+		{
+			if (mobj->script_stringargs[j] != NULL)
+			{
+				diff2 |= MD2_SPECIAL;
+				break;
+			}
+		}
+	}
 
 	// not the default but the most probable
 	if (mobj->momx != 0 || mobj->momy != 0 || mobj->momz != 0 || mobj->pmomz !=0)
@@ -5039,6 +5247,32 @@ static inline boolean P_UnArchiveLuabanksAndConsistency(void)
 	return true;
 }
 
+static void DoACSArchive(void)
+{
+	savebuffer_t save;
+	save.buffer = save_start;
+	save.end = save_end;
+	save.p = save_p;
+	save.size = save_length;
+
+	ACS_Archive(&save);
+
+	save_p = save.p;
+}
+
+static void DoACSUnArchive(void)
+{
+	savebuffer_t save;
+	save.buffer = save_start;
+	save.end = save_end;
+	save.p = save_p;
+	save.size = save_length;
+
+	ACS_UnArchive(&save);
+
+	save_p = save.p;
+}
+
 void P_SaveGame(INT16 mapnum)
 {
 	P_ArchiveMisc(mapnum);
@@ -5079,6 +5313,8 @@ void P_SaveNetGame(boolean resending)
 		P_NetArchiveWaypoints();
 		P_NetArchiveSectorPortals();
 	}
+
+	DoACSArchive();
 	LUA_Archive();
 
 	P_ArchiveLuabanksAndConsistency();
@@ -5122,6 +5358,8 @@ boolean P_LoadNetGame(boolean reloading)
 		P_RelinkPointers();
 		P_FinishMobjs();
 	}
+
+	DoACSUnArchive();
 	LUA_UnArchive();
 
 	// This is stupid and hacky, but maybe it'll work!
diff --git a/src/p_saveg.h b/src/p_saveg.h
index 545008e7efc6af656fe94864a4ebcf8ea28e5f27..b8e84daa8cab0824d16fb3cc4581021ba3e68549 100644
--- a/src/p_saveg.h
+++ b/src/p_saveg.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -14,8 +14,8 @@
 #ifndef __P_SAVEG__
 #define __P_SAVEG__
 
-#ifdef __GNUG__
-#pragma interface
+#ifdef __cplusplus
+extern "C" {
 #endif
 
 #define NEWSKINSAVES (INT16_MAX) // TODO: 2.3: Delete (Purely for backwards compatibility)
@@ -30,6 +30,14 @@ boolean P_LoadNetGame(boolean reloading);
 
 mobj_t *P_FindNewPosition(UINT32 oldposition);
 
+typedef struct
+{
+	UINT8 *buffer;
+	UINT8 *p;
+	UINT8 *end;
+	size_t size;
+} savebuffer_t;
+
 typedef struct
 {
 	UINT8 skin;
@@ -43,5 +51,12 @@ typedef struct
 
 extern savedata_t savedata;
 extern UINT8 *save_p;
+extern UINT8 *save_start;
+extern UINT8 *save_end;
+extern size_t save_length;
+
+#ifdef __cplusplus
+} // extern "C"
+#endif
 
 #endif
diff --git a/src/p_setup.c b/src/p_setup.c
index 41487d702f265c7e5164c61be8d0b8f8c059aa38..67d9d56cb7d482d4a54393cccb203852ed30253d 100644
--- a/src/p_setup.c
+++ b/src/p_setup.c
@@ -73,6 +73,8 @@
 #include "lua_script.h"
 #include "lua_hook.h"
 
+#include "acs/interface.h"
+
 #ifdef _WIN32
 #include <malloc.h>
 #include <math.h>
@@ -103,6 +105,7 @@ unsigned char mapmd5[16];
 //
 
 boolean udmf;
+static INT32 udmf_version;
 size_t numvertexes, numsegs, numsectors, numsubsectors, numnodes, numlines, numsides, nummapthings;
 vertex_t *vertexes;
 seg_t *segs;
@@ -1076,6 +1079,11 @@ static void P_LoadSectors(UINT8 *data)
 
 		ss->friction = ORIG_FRICTION;
 
+		ss->action = 0;
+		memset(ss->args, 0, NUM_SCRIPT_ARGS*sizeof(*ss->args));
+		memset(ss->stringargs, 0x00, NUM_SCRIPT_STRINGARGS*sizeof(*ss->stringargs));
+		ss->activation = 0;
+
 		P_InitializeSector(ss);
 	}
 }
@@ -1180,10 +1188,11 @@ static void P_LoadLinedefs(UINT8 *data)
 		ld->flags = SHORT(mld->flags);
 		ld->special = SHORT(mld->special);
 		Tag_FSet(&ld->tags, SHORT(mld->tag));
-		memset(ld->args, 0, NUMLINEARGS*sizeof(*ld->args));
-		memset(ld->stringargs, 0x00, NUMLINESTRINGARGS*sizeof(*ld->stringargs));
+		memset(ld->args, 0, NUM_SCRIPT_ARGS*sizeof(*ld->args));
+		memset(ld->stringargs, 0x00, NUM_SCRIPT_STRINGARGS*sizeof(*ld->stringargs));
 		ld->alpha = FRACUNIT;
 		ld->executordelay = 0;
+		ld->activation = 0;
 		P_SetLinedefV1(i, (UINT16)SHORT(mld->v1));
 		P_SetLinedefV2(i, (UINT16)SHORT(mld->v2));
 
@@ -1461,6 +1470,10 @@ static void P_LoadSidedefs(UINT8 *data)
 			case 459: // Control text prompt (named tag)
 			case 461: // Spawns an object on the map based on texture offsets
 			case 463: // Colorizes an object
+			case 475: // ACS_Execute
+			case 476: // ACS_ExecuteAlways
+			case 477: // ACS_Suspend
+			case 478: // ACS_Terminate
 			{
 				char process[8*3+1];
 				memset(process,0,8*3+1);
@@ -1531,11 +1544,15 @@ static void P_LoadThings(UINT8 *data)
 		mt->type = READUINT16(data);
 		mt->options = READUINT16(data);
 		mt->extrainfo = (UINT8)(mt->type >> 12);
+		mt->tid = 0;
 		Tag_FSet(&mt->tags, 0);
 		mt->scale = FRACUNIT;
 		mt->spritexscale = mt->spriteyscale = FRACUNIT;
-		memset(mt->args, 0, NUMMAPTHINGARGS*sizeof(*mt->args));
-		memset(mt->stringargs, 0x00, NUMMAPTHINGSTRINGARGS*sizeof(*mt->stringargs));
+		memset(mt->args, 0, NUM_MAPTHING_ARGS*sizeof(*mt->args));
+		memset(mt->stringargs, 0x00, NUM_MAPTHING_STRINGARGS*sizeof(*mt->stringargs));
+		mt->special = 0;
+		memset(mt->script_args, 0, NUM_SCRIPT_ARGS*sizeof(*mt->script_args));
+		memset(mt->script_stringargs, 0x00, NUM_SCRIPT_STRINGARGS*sizeof(*mt->script_stringargs));
 		mt->pitch = mt->roll = 0;
 
 		mt->type &= 4095;
@@ -1607,6 +1624,13 @@ static boolean TextmapCount(size_t size)
 			vertexesPos[numvertexes++] = M_TokenizerGetEndPos();
 		else if (fastcmp(tkn, "sector"))
 			sectorsPos[numsectors++] = M_TokenizerGetEndPos();
+		else if (fastcmp(tkn, "version"))
+		{
+			tkn = M_TokenizerRead(0);
+			udmf_version = atoi(tkn);
+			if (udmf_version > UDMF_CURRENT_VERSION)
+				CONS_Alert(CONS_WARNING, "Map is intended for future UDMF version '%d', current supported version is '%d'. This map may have issues loading.\n", udmf_version, UDMF_CURRENT_VERSION);
+		}
 		else
 			CONS_Alert(CONS_NOTICE, "Unknown field '%s'.\n", tkn);
 	}
@@ -1901,6 +1925,45 @@ static void ParseTextmapSectorParameter(UINT32 i, const char *param, const char
 		if (fastcmp(val, "Mobj"))
 			sectors[i].triggerer = TO_MOBJ;
 	}
+	else if (fastcmp(param, "action"))
+		sectors[i].action = atol(val);
+	else if (fastncmp(param, "stringarg", 9) && strlen(param) > 9)
+	{
+		size_t argnum = atol(param + 9);
+		if (argnum >= NUM_SCRIPT_STRINGARGS)
+			return;
+		sectors[i].stringargs[argnum] = Z_Malloc(strlen(val) + 1, PU_LEVEL, NULL);
+		M_Memcpy(sectors[i].stringargs[argnum], val, strlen(val) + 1);
+	}
+	else if (fastncmp(param, "arg", 3) && strlen(param) > 3)
+	{
+		size_t argnum = atol(param + 3);
+		if (argnum >= NUM_SCRIPT_ARGS)
+			return;
+		sectors[i].args[argnum] = atol(val);
+	}
+	else if (fastcmp(param, "repeatspecial") && fastcmp("true", val))
+		sectors[i].activation = (sectors[i].activation | ((sectors[i].activation & ~SECSPAC_TRIGGERMASK) | SECSPAC_REPEATSPECIAL));
+	else if (fastcmp(param, "continuousspecial") && fastcmp("true", val))
+		sectors[i].activation = (sectors[i].activation | ((sectors[i].activation & ~SECSPAC_TRIGGERMASK) | SECSPAC_CONTINUOUSSPECIAL));
+	else if (fastcmp(param, "playerenter") && fastcmp("true", val))
+		sectors[i].activation = (sectors[i].activation | SECSPAC_ENTER);
+	else if (fastcmp(param, "playerfloor") && fastcmp("true", val))
+		sectors[i].activation = (sectors[i].activation | SECSPAC_FLOOR);
+	else if (fastcmp(param, "playerceiling") && fastcmp("true", val))
+		sectors[i].activation = (sectors[i].activation | SECSPAC_CEILING);
+	else if (fastcmp(param, "monsterenter") && fastcmp("true", val))
+		sectors[i].activation = (sectors[i].activation | SECSPAC_ENTERMONSTER);
+	else if (fastcmp(param, "monsterfloor") && fastcmp("true", val))
+		sectors[i].activation = (sectors[i].activation | SECSPAC_FLOORMONSTER);
+	else if (fastcmp(param, "monsterceiling") && fastcmp("true", val))
+		sectors[i].activation = (sectors[i].activation | SECSPAC_CEILINGMONSTER);
+	else if (fastcmp(param, "missileenter") && fastcmp("true", val))
+		sectors[i].activation = (sectors[i].activation | SECSPAC_ENTERMISSILE);
+	else if (fastcmp(param, "missilefloor") && fastcmp("true", val))
+		sectors[i].activation = (sectors[i].activation | SECSPAC_FLOORMISSILE);
+	else if (fastcmp(param, "missileceiling") && fastcmp("true", val))
+		sectors[i].activation = (sectors[i].activation | SECSPAC_CEILINGMISSILE);
 }
 
 static void ParseTextmapSidedefParameter(UINT32 i, const char *param, const char *val)
@@ -1968,7 +2031,7 @@ static void ParseTextmapLinedefParameter(UINT32 i, const char *param, const char
 	else if (fastncmp(param, "stringarg", 9) && strlen(param) > 9)
 	{
 		size_t argnum = atol(param + 9);
-		if (argnum >= NUMLINESTRINGARGS)
+		if (argnum >= NUM_SCRIPT_STRINGARGS)
 			return;
 		lines[i].stringargs[argnum] = Z_Malloc(strlen(val) + 1, PU_LEVEL, NULL);
 		M_Memcpy(lines[i].stringargs[argnum], val, strlen(val) + 1);
@@ -1976,7 +2039,7 @@ static void ParseTextmapLinedefParameter(UINT32 i, const char *param, const char
 	else if (fastncmp(param, "arg", 3) && strlen(param) > 3)
 	{
 		size_t argnum = atol(param + 3);
-		if (argnum >= NUMLINEARGS)
+		if (argnum >= NUM_SCRIPT_ARGS)
 			return;
 		lines[i].args[argnum] = atol(val);
 	}
@@ -2037,12 +2100,30 @@ static void ParseTextmapLinedefParameter(UINT32 i, const char *param, const char
 		lines[i].flags |= ML_BOUNCY;
 	else if (fastcmp(param, "transfer") && fastcmp("true", val))
 		lines[i].flags |= ML_TFERLINE;
+	// Activation flags
+	else if (fastcmp(param, "repeatspecial") && fastcmp("true", val))
+		lines[i].activation |= SPAC_REPEATSPECIAL;
+	else if (fastcmp(param, "playercross") && fastcmp("true", val))
+		lines[i].activation |= SPAC_CROSS;
+	else if (fastcmp(param, "monstercross") && fastcmp("true", val))
+		lines[i].activation |= SPAC_CROSSMONSTER;
+	else if (fastcmp(param, "missilecross") && fastcmp("true", val))
+		lines[i].activation |= SPAC_CROSSMISSILE;
+	else if (fastcmp(param, "playerpush") && fastcmp("true", val))
+		lines[i].activation |= SPAC_PUSH;
+	else if (fastcmp(param, "monsterpush") && fastcmp("true", val))
+		lines[i].activation |= SPAC_PUSHMONSTER;
+	else if (fastcmp(param, "impact") && fastcmp("true", val))
+		lines[i].activation |= SPAC_IMPACT;
 }
 
 static void ParseTextmapThingParameter(UINT32 i, const char *param, const char *val)
 {
 	if (fastcmp(param, "id"))
+	{
+		mapthings[i].tid = atol(val);
 		Tag_FSet(&mapthings[i].tags, atol(val));
+	}
 	else if (fastcmp(param, "moreids"))
 	{
 		const char* id = val;
@@ -2084,7 +2165,7 @@ static void ParseTextmapThingParameter(UINT32 i, const char *param, const char *
 	else if (fastncmp(param, "stringarg", 9) && strlen(param) > 9)
 	{
 		size_t argnum = atol(param + 9);
-		if (argnum >= NUMMAPTHINGSTRINGARGS)
+		if (argnum >= NUM_MAPTHING_STRINGARGS)
 			return;
 		mapthings[i].stringargs[argnum] = Z_Malloc(strlen(val) + 1, PU_LEVEL, NULL);
 		M_Memcpy(mapthings[i].stringargs[argnum], val, strlen(val) + 1);
@@ -2092,10 +2173,47 @@ static void ParseTextmapThingParameter(UINT32 i, const char *param, const char *
 	else if (fastncmp(param, "arg", 3) && strlen(param) > 3)
 	{
 		size_t argnum = atol(param + 3);
-		if (argnum >= NUMMAPTHINGARGS)
+		if (argnum >= NUM_MAPTHING_ARGS)
+			return;
+		mapthings[i].args[argnum] = atol(val);
+	}
+
+	else if (fastcmp(param, "special"))
+		mapthings[i].special = atol(val);
+	else if (fastncmp(param, "scriptstringarg", 9) && strlen(param) > 9)
+	{
+		size_t argnum = atol(param + 9);
+		if (argnum >= NUM_SCRIPT_STRINGARGS)
+			return;
+		mapthings[i].script_stringargs[argnum] = Z_Malloc(strlen(val) + 1, PU_LEVEL, NULL);
+		M_Memcpy(mapthings[i].script_stringargs[argnum], val, strlen(val) + 1);
+	}
+	else if (fastncmp(param, "scriptarg", 3) && strlen(param) > 3)
+	{
+		size_t argnum = atol(param + 3);
+		if (argnum >= NUM_SCRIPT_ARGS)
+			return;
+		mapthings[i].script_args[argnum] = atol(val);
+	}
+#if 0
+	else if (fastncmp(param, "thingstringarg", 14) && strlen(param) > 14)
+	{
+		size_t argnum = atol(param + 14);
+		if (argnum >= NUM_MAPTHING_STRINGARGS)
+			return;
+		size_t len = strlen(val);
+		mapthings[i].stringargs[argnum] = Z_Malloc(len + 1, PU_LEVEL, NULL);
+		M_Memcpy(mapthings[i].stringargs[argnum], val, len);
+		mapthings[i].stringargs[argnum][len] = '\0';
+	}
+	else if (fastncmp(param, "thingarg", 8) && strlen(param) > 8)
+	{
+		size_t argnum = atol(param + 8);
+		if (argnum >= NUM_MAPTHING_ARGS)
 			return;
 		mapthings[i].args[argnum] = atol(val);
 	}
+#endif
 }
 
 /** From a given position table, run a specified parser function through a {}-encapsuled text.
@@ -2200,16 +2318,14 @@ typedef struct
 static void P_WriteTextmap_Things(FILE *f, const mapthing_t *wmapthings)
 {
 	size_t i, j;
-	mtag_t firsttag;
 
 	// Actual writing
 	for (i = 0; i < nummapthings; i++)
 	{
 		fprintf(f, "thing // %s\n", sizeu1(i));
 		fprintf(f, "{\n");
-		firsttag = Tag_FGet(&wmapthings[i].tags);
-		if (firsttag != 0)
-			fprintf(f, "id = %d;\n", firsttag);
+		if (wmapthings[i].tid != 0)
+			fprintf(f, "id = %d;\n", wmapthings[i].tid);
 		if (wmapthings[i].tags.count > 1)
 		{
 			fprintf(f, "moreids = \"");
@@ -2240,12 +2356,21 @@ static void P_WriteTextmap_Things(FILE *f, const mapthing_t *wmapthings)
 			fprintf(f, "mobjscale = %f;\n", FIXED_TO_FLOAT(wmapthings[i].scale));
 		if (wmapthings[i].options & MTF_OBJECTFLIP)
 			fprintf(f, "flip = true;\n");
-		for (j = 0; j < NUMMAPTHINGARGS; j++)
+		if (wmapthings[i].special != 0)
+			fprintf(f, "special = %d;\n", wmapthings[i].special);
+		for (j = 0; j < NUM_SCRIPT_ARGS; j++)
+			if (wmapthings[i].script_args[j] != 0)
+				fprintf(f, "arg%s = %d;\n", sizeu1(j), wmapthings[i].script_args[j]);
+		for (j = 0; j < NUM_SCRIPT_STRINGARGS; j++)
+			if (mapthings[i].script_stringargs[j])
+				fprintf(f, "stringarg%s = \"%s\";\n", sizeu1(j), mapthings[i].script_stringargs[j]);
+		for (j = 0; j < NUM_MAPTHING_ARGS; j++)
 			if (wmapthings[i].args[j] != 0)
-				fprintf(f, "arg%s = %d;\n", sizeu1(j), wmapthings[i].args[j]);
-		for (j = 0; j < NUMMAPTHINGSTRINGARGS; j++)
+				fprintf(f, "thingarg%s = %d;\n", sizeu1(j), wmapthings[i].args[j]);
+		for (j = 0; j < NUM_MAPTHING_STRINGARGS; j++)
 			if (mapthings[i].stringargs[j])
-				fprintf(f, "stringarg%s = \"%s\";\n", sizeu1(j), mapthings[i].stringargs[j]);
+				fprintf(f, "thingstringarg%s = \"%s\";\n", sizeu1(j), mapthings[i].stringargs[j]);
+
 		fprintf(f, "}\n");
 		fprintf(f, "\n");
 	}
@@ -2518,6 +2643,7 @@ static void P_WriteTextmap(void)
 	}
 
 	fprintf(f, "namespace = \"srb2\";\n");
+	fprintf(f, "version = %d;\n", UDMF_CURRENT_VERSION);
 	P_WriteTextmap_Things(f, wmapthings);
 
 	for (i = 0; i < numvertexes; i++)
@@ -2559,10 +2685,10 @@ static void P_WriteTextmap(void)
 		}
 		if (wlines[i].special != 0)
 			fprintf(f, "special = %d;\n", wlines[i].special);
-		for (j = 0; j < NUMLINEARGS; j++)
+		for (j = 0; j < NUM_SCRIPT_ARGS; j++)
 			if (wlines[i].args[j] != 0)
 				fprintf(f, "arg%s = %d;\n", sizeu1(j), wlines[i].args[j]);
-		for (j = 0; j < NUMLINESTRINGARGS; j++)
+		for (j = 0; j < NUM_SCRIPT_STRINGARGS; j++)
 			if (lines[i].stringargs[j])
 				fprintf(f, "stringarg%s = \"%s\";\n", sizeu1(j), lines[i].stringargs[j]);
 		if (wlines[i].alpha != FRACUNIT)
@@ -2625,6 +2751,20 @@ static void P_WriteTextmap(void)
 			fprintf(f, "bouncy = true;\n");
 		if (wlines[i].flags & ML_TFERLINE)
 			fprintf(f, "transfer = true;\n");
+		if (wlines[i].activation & SPAC_REPEATSPECIAL)
+			fprintf(f, "repeatspecial = true;\n");
+		if (wlines[i].activation & SPAC_CROSS)
+			fprintf(f, "playercross = true;\n");
+		if (wlines[i].activation & SPAC_CROSSMONSTER)
+			fprintf(f, "monstercross = true;\n");
+		if (wlines[i].activation & SPAC_CROSSMISSILE)
+			fprintf(f, "missilecross = true;\n");
+		if (wlines[i].activation & SPAC_PUSH)
+			fprintf(f, "playerpush = true;\n");
+		if (wlines[i].activation & SPAC_PUSHMONSTER)
+			fprintf(f, "monsterpush = true;\n");
+		if (wlines[i].activation & SPAC_IMPACT)
+			fprintf(f, "impact = true;\n");
 		fprintf(f, "}\n");
 		fprintf(f, "\n");
 	}
@@ -2879,6 +3019,45 @@ static void P_WriteTextmap(void)
 					break;
 			}
 		}
+		if (wsectors[i].action != 0)
+			fprintf(f, "action = %d;\n", wsectors[i].action);
+		for (j = 0; j < NUM_SCRIPT_ARGS; j++)
+			if (wsectors[i].args[j] != 0)
+				fprintf(f, "arg%s = %d;\n", sizeu1(j), wsectors[i].args[j]);
+		for (j = 0; j < NUM_SCRIPT_STRINGARGS; j++)
+			if (wsectors[i].stringargs[j])
+				fprintf(f, "stringarg%s = \"%s\";\n", sizeu1(j), wsectors[i].stringargs[j]);
+		switch (wsectors[i].activation & SECSPAC_TRIGGERMASK)
+		{
+			case SECSPAC_REPEATSPECIAL:
+			{
+				fprintf(f, "repeatspecial = true;\n");
+				break;
+			}
+			case SECSPAC_CONTINUOUSSPECIAL:
+			{
+				fprintf(f, "continuousspecial = true;\n");
+				break;
+			}
+		}
+		if (wsectors[i].activation & SECSPAC_ENTER)
+			fprintf(f, "playerenter = true;\n");
+		if (wsectors[i].activation & SECSPAC_FLOOR)
+			fprintf(f, "playerfloor = true;\n");
+		if (wsectors[i].activation & SECSPAC_CEILING)
+			fprintf(f, "playerceiling = true;\n");
+		if (wsectors[i].activation & SECSPAC_ENTERMONSTER)
+			fprintf(f, "monsterenter = true;\n");
+		if (wsectors[i].activation & SECSPAC_FLOORMONSTER)
+			fprintf(f, "monsterfloor = true;\n");
+		if (wsectors[i].activation & SECSPAC_CEILINGMONSTER)
+			fprintf(f, "monsterceiling = true;\n");
+		if (wsectors[i].activation & SECSPAC_ENTERMISSILE)
+			fprintf(f, "missileenter = true;\n");
+		if (wsectors[i].activation & SECSPAC_FLOORMISSILE)
+			fprintf(f, "missilefloor = true;\n");
+		if (wsectors[i].activation & SECSPAC_CEILINGMISSILE)
+			fprintf(f, "missileceiling = true;\n");
 		fprintf(f, "}\n");
 		fprintf(f, "\n");
 	}
@@ -2985,6 +3164,11 @@ static void P_LoadTextmap(void)
 
 		sc->friction = ORIG_FRICTION;
 
+		sc->action = 0;
+		memset(sc->args, 0, NUM_SCRIPT_ARGS*sizeof(*sc->args));
+		memset(sc->stringargs, 0x00, NUM_SCRIPT_STRINGARGS*sizeof(*sc->stringargs));
+		sc->activation = 0;
+
 		textmap_colormap.used = false;
 		textmap_colormap.lightcolor = 0;
 		textmap_colormap.lightalpha = 25;
@@ -3039,12 +3223,13 @@ static void P_LoadTextmap(void)
 		ld->special = 0;
 		Tag_FSet(&ld->tags, 0);
 
-		memset(ld->args, 0, NUMLINEARGS*sizeof(*ld->args));
-		memset(ld->stringargs, 0x00, NUMLINESTRINGARGS*sizeof(*ld->stringargs));
+		memset(ld->args, 0, NUM_SCRIPT_ARGS*sizeof(*ld->args));
+		memset(ld->stringargs, 0x00, NUM_SCRIPT_STRINGARGS*sizeof(*ld->stringargs));
 		ld->alpha = FRACUNIT;
 		ld->executordelay = 0;
 		ld->sidenum[0] = NO_SIDEDEF;
 		ld->sidenum[1] = NO_SIDEDEF;
+		ld->activation = 0;
 
 		TextmapParse(linesPos[i], i, ParseTextmapLinedefParameter);
 
@@ -3090,11 +3275,15 @@ static void P_LoadTextmap(void)
 		mt->options = 0;
 		mt->z = 0;
 		mt->extrainfo = 0;
+		mt->tid = 0;
 		Tag_FSet(&mt->tags, 0);
 		mt->scale = FRACUNIT;
 		mt->spritexscale = mt->spriteyscale = FRACUNIT;
-		memset(mt->args, 0, NUMMAPTHINGARGS*sizeof(*mt->args));
-		memset(mt->stringargs, 0x00, NUMMAPTHINGSTRINGARGS*sizeof(*mt->stringargs));
+		memset(mt->args, 0, NUM_MAPTHING_ARGS*sizeof(*mt->args));
+		memset(mt->stringargs, 0x00, NUM_MAPTHING_STRINGARGS*sizeof(*mt->stringargs));
+		mt->special = 0;
+		memset(mt->script_args, 0, NUM_SCRIPT_ARGS*sizeof(*mt->script_args));
+		memset(mt->script_stringargs, 0x00, NUM_SCRIPT_STRINGARGS*sizeof(*mt->script_stringargs));
 		mt->mobj = NULL;
 
 		TextmapParse(mapthingsPos[i], i, ParseTextmapThingParameter);
@@ -7048,7 +7237,9 @@ static boolean P_LoadMapFromFile(void)
 	virtres_t *virt = vres_GetMap(lastloadedmaplumpnum);
 	virtlump_t *textmap = vres_Find(virt, "TEXTMAP");
 	size_t i;
+
 	udmf = textmap != NULL;
+	udmf_version = 0;
 
 	if (!P_LoadMapData(virt))
 		return false;
@@ -7887,6 +8078,8 @@ boolean P_LoadLevel(boolean fromnetsave, boolean reloadinggamestate)
 	// Close text prompt before freeing the old level
 	F_EndTextPrompt(false, true);
 
+	ACS_InvalidateMapScope();
+
 	LUA_InvalidateLevel();
 
 	for (ss = sectors; sectors+numsectors != ss; ss++)
@@ -7995,8 +8188,13 @@ boolean P_LoadLevel(boolean fromnetsave, boolean reloadinggamestate)
 	//  a netgame save is being loaded, and could actively be harmful by messing with
 	//  the client's view of the data.)
 	if (!fromnetsave)
+	{
 		P_InitGametype();
 
+		// Initialize ACS scripts
+		ACS_LoadLevelScripts(gamemap);
+	}
+
 	if (!reloadinggamestate)
 	{
 		P_InitCamera();
@@ -8021,8 +8219,6 @@ boolean P_LoadLevel(boolean fromnetsave, boolean reloadinggamestate)
 
 	P_RunCachedActions();
 
-	P_MapEnd(); // tmthing is no longer needed from this point onwards
-
 	// Took me 3 hours to figure out why my progression kept on getting overwritten with the titlemap...
 	if (!titlemapinaction)
 	{
@@ -8039,6 +8235,20 @@ boolean P_LoadLevel(boolean fromnetsave, boolean reloadinggamestate)
 		lastmaploaded = gamemap; // HAS to be set after saving!!
 	}
 
+	ACS_RunLevelStartScripts();
+
+	for (i = 0; i < MAXPLAYERS; i++)
+	{
+		if (!playeringame[i])
+			continue;
+		if (players[i].spectator)
+			continue;
+		ACS_RunPlayerEnterScript(&players[i]);
+		players[i].enteredgame = true;
+	}
+
+	P_MapEnd(); // tmthing is no longer needed from this point onwards
+
 	if (!fromnetsave) // uglier hack
 	{ // to make a newly loaded level start on the second frame.
 		INT32 buf = gametic % BACKUPTICS;
diff --git a/src/p_setup.h b/src/p_setup.h
index da38d4c08b152f87fab59517706c3ccbc7544339..b797920c0801e0b0193b24b878ee79738adc615c 100644
--- a/src/p_setup.h
+++ b/src/p_setup.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -18,6 +18,10 @@
 #include "doomstat.h"
 #include "r_defs.h"
 
+#ifdef __cplusplus
+extern "C" {
+#endif
+
 // map md5, sent to players via PT_SERVERINFO
 extern unsigned char mapmd5[16];
 
@@ -89,4 +93,8 @@ UINT32 P_GetScoreForGrade(INT16 map, UINT8 mare, UINT8 grade);
 UINT32 P_GetScoreForGradeOverall(INT16 map, UINT8 grade);
 void P_AddNiGHTSTimes(INT16 i, char *gtext);
 
+#ifdef __cplusplus
+} // extern "C"
+#endif
+
 #endif
diff --git a/src/p_spec.c b/src/p_spec.c
index 805817fb033c465b33059c24fbefb52432173444..6dc5e0ee8aefb85423898fe125f95ad895c7c0f8 100644
--- a/src/p_spec.c
+++ b/src/p_spec.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -38,6 +38,7 @@
 #include "m_misc.h"
 #include "m_cond.h" //unlock triggers
 #include "lua_hook.h" // LUA_HookLinedefExecute
+#include "acs/interface.h"
 #include "f_finale.h" // control text prompt
 #include "r_skins.h" // skins
 
@@ -1565,6 +1566,11 @@ static boolean P_CheckEmeralds(INT32 checktype, UINT16 target)
 	}
 }
 
+boolean P_CanActivateSpecial(INT16 special)
+{
+	return (special >= 400 && special < 500);
+}
+
 static void P_ActivateLinedefExecutor(line_t *line, mobj_t *actor, sector_t *caller)
 {
 	if (line->special < 400 || line->special >= 500)
@@ -2155,6 +2161,226 @@ void P_SwitchWeather(INT32 weathernum)
 	}
 }
 
+static void P_LineSpecialWasActivated(line_t *line)
+{
+	if (!(line->activation & SPAC_REPEATSPECIAL))
+	{
+		line->special = 0;
+	}
+}
+
+static boolean P_AllowSpecialCross(line_t *line, mobj_t *thing)
+{
+	if (P_CanActivateSpecial(line->special) == false)
+	{
+		// No special to even activate.
+		return false;
+	}
+
+	if (thing->player != NULL)
+	{
+		return !!(line->activation & SPAC_CROSS);
+	}
+	else if ((thing->flags & (MF_ENEMY|MF_BOSS)) != 0)
+	{
+		return !!(line->activation & SPAC_CROSSMONSTER);
+	}
+	else if (thing->flags & MF_MISSILE)
+	{
+		return !!(line->activation & SPAC_CROSSMISSILE);
+	}
+
+	// No activation flags for you.
+	return false;
+}
+
+//
+// P_CrossSpecialLine - TRIGGER
+// Called every time a thing origin is about
+//  to cross a line with specific specials
+//
+void P_CrossSpecialLine(line_t *line, INT32 side, mobj_t *thing)
+{
+	player_t *player = NULL;
+	activator_t *activator = NULL;
+	boolean result = false;
+
+	if (thing == NULL || P_MobjWasRemoved(thing) == true || thing->health <= 0)
+	{
+		// Invalid mobj.
+		return;
+	}
+
+	player = thing->player;
+
+	if (player != NULL)
+	{
+		if (player->spectator == true)
+		{
+			// Ignore spectators.
+			return;
+		}
+	}
+
+	if (P_AllowSpecialCross(line, thing) == false)
+	{
+		// This special can't be activated this way.
+		return;
+	}
+
+	activator = Z_Calloc(sizeof(activator_t), PU_LEVEL, NULL);
+	I_Assert(activator != NULL);
+
+	P_SetTarget(&activator->mo, thing);
+	activator->line = line;
+	activator->side = side;
+	activator->sector = (side != 0) ? line->backsector : line->frontsector;
+
+	result = P_ProcessSpecial(activator, line->special, line->args, line->stringargs);
+
+	P_SetTarget(&activator->mo, NULL);
+	Z_Free(activator);
+
+	if (result == true)
+	{
+		P_LineSpecialWasActivated(line);
+	}
+}
+
+static boolean P_AllowSpecialPush(line_t *line, mobj_t *thing)
+{
+	if (P_CanActivateSpecial(line->special) == false)
+	{
+		// No special to even activate.
+		return false;
+	}
+
+	if (thing->player != NULL)
+	{
+		return !!(line->activation & SPAC_PUSH);
+	}
+	else if ((thing->flags & (MF_ENEMY|MF_BOSS)) != 0)
+	{
+		return !!(line->activation & SPAC_PUSHMONSTER);
+	}
+	else if (thing->flags & MF_MISSILE)
+	{
+		return !!(line->activation & SPAC_IMPACT);
+	}
+
+	// No activation flags for you.
+	return false;
+}
+
+//
+// P_PushSpecialLine - TRIGGER
+// Called every time a thing origin is blocked
+//  by a line with specific specials
+//
+void P_PushSpecialLine(line_t *line, mobj_t *thing)
+{
+	player_t *player = NULL;
+	activator_t *activator = NULL;
+	boolean result = false;
+
+	if (thing == NULL || P_MobjWasRemoved(thing) == true || thing->health <= 0)
+	{
+		// Invalid mobj.
+		return;
+	}
+
+	if (line == NULL)
+	{
+		// Invalid line.
+		return;
+	}
+
+	player = thing->player;
+
+	if (player != NULL)
+	{
+		if (player->spectator == true)
+		{
+			// Ignore spectators.
+			return;
+		}
+	}
+
+	if (P_AllowSpecialPush(line, thing) == false)
+	{
+		// This special can't be activated this way.
+		return;
+	}
+
+	activator = Z_Calloc(sizeof(activator_t), PU_LEVEL, NULL);
+	I_Assert(activator != NULL);
+
+	P_SetTarget(&activator->mo, thing);
+	activator->line = line;
+	activator->side = P_PointOnLineSide(thing->x, thing->y, line);
+	activator->sector = (activator->side != 0) ? line->backsector : line->frontsector;
+
+	result = P_ProcessSpecial(activator, line->special, line->args, line->stringargs);
+
+	P_SetTarget(&activator->mo, NULL);
+	Z_Free(activator);
+
+	if (result == true)
+	{
+		P_LineSpecialWasActivated(line);
+	}
+}
+
+//
+// P_ActivateThingSpecial - TRIGGER
+// Called when a thing is killed, or upon
+//  any other type-specific conditions
+//
+void P_ActivateThingSpecial(mobj_t *mo, mobj_t *source)
+{
+	player_t *player = NULL;
+	activator_t *activator = NULL;
+
+	if (mo == NULL || P_MobjWasRemoved(mo) == true)
+	{
+		// Invalid mobj.
+		return;
+	}
+
+	// Is this necessary? Probably not, but I hate
+	// spectators so I will manually ensure they
+	// can't impact the gamestate anyway.
+	player = mo->player;
+	if (player != NULL)
+	{
+		if (player->spectator == true)
+		{
+			// Ignore spectators.
+			return;
+		}
+	}
+
+	if (P_CanActivateSpecial(mo->special) == false)
+	{
+		// No special to even activate.
+		return;
+	}
+
+	activator = Z_Calloc(sizeof(activator_t), PU_LEVEL, NULL);
+	I_Assert(activator != NULL);
+
+	if (source != NULL)
+	{
+		P_SetTarget(&activator->mo, source);
+		activator->sector = source->subsector->sector;
+	}
+
+	P_ProcessSpecial(activator, mo->special, mo->script_args, mo->script_stringargs);
+
+	P_SetTarget(&activator->mo, NULL);
+	Z_Free(activator);
+}
+
 /** Gets an object.
   *
   * \param type Object type to look for.
@@ -2176,7 +2402,7 @@ static mobj_t *P_GetObjectTypeInSectorNum(mobjtype_t type, size_t s)
 	return NULL;
 }
 
-static mobj_t* P_FindObjectTypeFromTag(mobjtype_t type, mtag_t tag)
+mobj_t* P_FindObjectTypeFromTag(mobjtype_t type, mtag_t tag)
 {
 	if (udmf)
 	{
@@ -2225,6 +2451,35 @@ static mobj_t* P_FindObjectTypeFromTag(mobjtype_t type, mtag_t tag)
   */
 static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 {
+	activator_t *activator = NULL;
+
+	if (line == NULL)
+	{
+		// No line to activate
+		return;
+	}
+
+	activator = Z_Calloc(sizeof(activator_t), PU_LEVEL, NULL);
+	I_Assert(activator != NULL);
+
+	P_SetTarget(&activator->mo, mo);
+	activator->line = line;
+	activator->sector = callsec;
+
+	P_ProcessSpecial(activator, line->special, line->args, line->stringargs);
+
+	P_SetTarget(&activator->mo, NULL);
+	Z_Free(activator);
+
+	// Intentionally no P_LineSpecialWasActivated call.
+}
+
+boolean P_ProcessSpecial(activator_t *activator, INT16 special, INT32 *args, char **stringargs)
+{
+	line_t *line = activator->line; // If called from a linedef executor, this is the control sector linedef. If from a script, then it's the actual activator.
+	mobj_t *mo = activator->mo;
+	sector_t *callsec = activator->sector;
+
 	INT32 secnum = -1;
 	mobj_t *bot = NULL;
 
@@ -2234,13 +2489,13 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 		bot = players[secondarydisplayplayer].mo;
 
 	// note: only commands with linedef types >= 400 && < 500 can be used
-	switch (line->special)
+	switch (special)
 	{
 		case 400: // Set tagged sector's heights/flats
-			if (line->args[1] != TMP_CEILING)
-				EV_DoFloor(line->args[0], line, instantMoveFloorByFrontSector);
-			if (line->args[1] != TMP_FLOOR)
-				EV_DoCeiling(line->args[0], line, instantMoveCeilingByFrontSector);
+			if (args[1] != TMP_CEILING)
+				EV_DoFloor(args[0], line, instantMoveFloorByFrontSector);
+			if (args[1] != TMP_FLOOR)
+				EV_DoCeiling(args[0], line, instantMoveCeilingByFrontSector);
 			break;
 
 		case 402: // Copy light level to tagged sectors
@@ -2258,7 +2513,7 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 				newfloorlightsec = line->frontsector->floorlightsec;
 				newceilinglightsec = line->frontsector->ceilinglightsec;
 
-				TAG_ITER_SECTORS(line->args[0], secnum)
+				TAG_ITER_SECTORS(args[0], secnum)
 				{
 					if (sectors[secnum].lightingdata)
 					{
@@ -2267,15 +2522,15 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 						sectors[secnum].lightingdata = NULL;
 					}
 
-					if (!(line->args[1] & TMLC_NOSECTOR))
+					if (!(args[1] & TMLC_NOSECTOR))
 						sectors[secnum].lightlevel = newlightlevel;
-					if (!(line->args[1] & TMLC_NOFLOOR))
+					if (!(args[1] & TMLC_NOFLOOR))
 					{
 						sectors[secnum].floorlightlevel = newfloorlightlevel;
 						sectors[secnum].floorlightabsolute = newfloorlightabsolute;
 						sectors[secnum].floorlightsec = newfloorlightsec;
 					}
-					if (!(line->args[1] & TMLC_NOCEILING))
+					if (!(args[1] & TMLC_NOCEILING))
 					{
 						sectors[secnum].ceilinglightlevel = newceilinglightlevel;
 						sectors[secnum].ceilinglightabsolute = newceilinglightabsolute;
@@ -2286,26 +2541,26 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 			break;
 
 		case 403: // Move planes by front sector
-			if (line->args[1] != TMP_CEILING)
-				EV_DoFloor(line->args[0], line, moveFloorByFrontSector);
-			if (line->args[1] != TMP_FLOOR)
-				EV_DoCeiling(line->args[0], line, moveCeilingByFrontSector);
+			if (args[1] != TMP_CEILING)
+				EV_DoFloor(args[0], line, moveFloorByFrontSector);
+			if (args[1] != TMP_FLOOR)
+				EV_DoCeiling(args[0], line, moveCeilingByFrontSector);
 			break;
 
 		case 405: // Move planes by distance
-			if (line->args[1] != TMP_CEILING)
-				EV_DoFloor(line->args[0], line, moveFloorByDistance);
-			if (line->args[1] != TMP_FLOOR)
-				EV_DoCeiling(line->args[0], line, moveCeilingByDistance);
+			if (args[1] != TMP_CEILING)
+				EV_DoFloor(args[0], line, moveFloorByDistance);
+			if (args[1] != TMP_FLOOR)
+				EV_DoCeiling(args[0], line, moveCeilingByDistance);
 			break;
 
 		case 408: // Set flats
 		{
-			TAG_ITER_SECTORS(line->args[0], secnum)
+			TAG_ITER_SECTORS(args[0], secnum)
 			{
-				if (line->args[1] != TMP_CEILING)
+				if (args[1] != TMP_CEILING)
 					sectors[secnum].floorpic = line->frontsector->floorpic;
-				if (line->args[1] != TMP_FLOOR)
+				if (args[1] != TMP_FLOOR)
 					sectors[secnum].ceilingpic = line->frontsector->ceilingpic;
 			}
 			break;
@@ -2314,11 +2569,11 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 		case 409: // Change tagged sectors' tag
 		// (formerly "Change calling sectors' tag", but behavior was changed)
 		{
-			mtag_t newtag = line->args[1];
+			mtag_t newtag = args[1];
 
-			TAG_ITER_SECTORS(line->args[0], secnum)
+			TAG_ITER_SECTORS(args[0], secnum)
 			{
-				switch (line->args[2])
+				switch (args[2])
 				{
 					case TMT_ADD:
 						Tag_SectorAdd(secnum, newtag);
@@ -2340,10 +2595,10 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 
 		case 410: // Change front sector's tag
 		{
-			mtag_t newtag = line->args[1];
+			mtag_t newtag = args[1];
 			secnum = (UINT32)(line->frontsector - sectors);
 
-			switch (line->args[2])
+			switch (args[2])
 			{
 				case TMT_ADD:
 					Tag_SectorAdd(secnum, newtag);
@@ -2363,7 +2618,7 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 		}
 
 		case 411: // Stop floor/ceiling movement in tagged sector(s)
-			TAG_ITER_SECTORS(line->args[0], secnum)
+			TAG_ITER_SECTORS(args[0], secnum)
 			{
 				if (sectors[secnum].floordata)
 				{
@@ -2395,15 +2650,15 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 				mobj_t *dest;
 
 				if (!mo) // nothing to teleport
-					return;
+					return false;
 
-				if (line->args[1] & TMT_RELATIVE) // Relative silent teleport
+				if (args[1] & TMT_RELATIVE) // Relative silent teleport
 				{
 					fixed_t x, y, z;
 
-					x = line->args[2] << FRACBITS;
-					y = line->args[3] << FRACBITS;
-					z = line->args[4] << FRACBITS;
+					x = args[2] << FRACBITS;
+					y = args[3] << FRACBITS;
+					z = args[4] << FRACBITS;
 
 					P_UnsetThingPosition(mo);
 					mo->x += x;
@@ -2436,13 +2691,13 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 					angle_t angle;
 					boolean silent, keepmomentum;
 
-					dest = P_FindObjectTypeFromTag(MT_TELEPORTMAN, line->args[0]);
+					dest = P_FindObjectTypeFromTag(MT_TELEPORTMAN, args[0]);
 					if (!dest)
-						return;
+						return false;
 
-					angle = (line->args[1] & TMT_KEEPANGLE) ? mo->angle : dest->angle;
-					silent = !!(line->args[1] & TMT_SILENT);
-					keepmomentum = !!(line->args[1] & TMT_KEEPMOMENTUM);
+					angle = (args[1] & TMT_KEEPANGLE) ? mo->angle : dest->angle;
+					silent = !!(args[1] & TMT_SILENT);
+					keepmomentum = !!(args[1] & TMT_KEEPMOMENTUM);
 
 					if (bot)
 						P_Teleport(bot, dest->x, dest->y, dest->z, angle, !silent, keepmomentum);
@@ -2455,18 +2710,18 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 
 		case 413: // Change music
 			// console player only unless TMM_ALLPLAYERS is set
-			if ((line->args[0] & TMM_ALLPLAYERS) || (mo && mo->player && P_IsLocalPlayer(mo->player)) || titlemapinaction)
+			if ((args[0] & TMM_ALLPLAYERS) || (mo && mo->player && P_IsLocalPlayer(mo->player)) || titlemapinaction)
 			{
-				boolean musicsame = (!line->stringargs[0] || !line->stringargs[0][0] || !strnicmp(line->stringargs[0], S_MusicName(), 7));
-				UINT16 tracknum = (UINT16)max(line->args[6], 0);
-				INT32 position = (INT32)max(line->args[1], 0);
-				UINT32 prefadems = (UINT32)max(line->args[2], 0);
-				UINT32 postfadems = (UINT32)max(line->args[3], 0);
-				UINT8 fadetarget = (UINT8)max(line->args[4], 0);
-				INT16 fadesource = (INT16)max(line->args[5], -1);
+				boolean musicsame = (!stringargs[0] || !stringargs[0][0] || !strnicmp(stringargs[0], S_MusicName(), 7));
+				UINT16 tracknum = (UINT16)max(args[6], 0);
+				INT32 position = (INT32)max(args[1], 0);
+				UINT32 prefadems = (UINT32)max(args[2], 0);
+				UINT32 postfadems = (UINT32)max(args[3], 0);
+				UINT8 fadetarget = (UINT8)max(args[4], 0);
+				INT16 fadesource = (INT16)max(args[5], -1);
 
 				// Seek offset from current song position
-				if (line->args[0] & TMM_OFFSET)
+				if (args[0] & TMM_OFFSET)
 				{
 					// adjust for loop point if subtracting
 					if (position < 0 && S_GetMusicLength() &&
@@ -2478,7 +2733,7 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 				}
 
 				// Fade current music to target volume (if music won't be changed)
-				if ((line->args[0] & TMM_FADE) && fadetarget && musicsame)
+				if ((args[0] & TMM_FADE) && fadetarget && musicsame)
 				{
 					// 0 fadesource means fade from current volume.
 					// meaning that we can't specify volume 0 as the source volume -- this starts at 1.
@@ -2496,27 +2751,27 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 				// Change the music and apply position/fade operations
 				else
 				{
-					if (!line->stringargs[0] || !strcmp(line->stringargs[0], "-"))
+					if (!stringargs[0] || !strcmp(stringargs[0], "-"))
 						strcpy(mapmusname, "");
 					else
 					{
-						strncpy(mapmusname, line->stringargs[0], 7);
+						strncpy(mapmusname, stringargs[0], 7);
 						mapmusname[6] = 0;
 					}
 
 					mapmusflags = tracknum & MUSIC_TRACKMASK;
-					if (!(line->args[0] & TMM_NORELOAD))
+					if (!(args[0] & TMM_NORELOAD))
 						mapmusflags |= MUSIC_RELOADRESET;
-					if (line->args[0] & TMM_FORCERESET)
+					if (args[0] & TMM_FORCERESET)
 						mapmusflags |= MUSIC_FORCERESET;
 
 					mapmusposition = position;
 
-					S_ChangeMusicEx(mapmusname, mapmusflags, !(line->args[0] & TMM_NOLOOP), position,
-						!(line->args[0] & TMM_FADE) ? prefadems : 0,
-						!(line->args[0] & TMM_FADE) ? postfadems : 0);
+					S_ChangeMusicEx(mapmusname, mapmusflags, !(args[0] & TMM_NOLOOP), position,
+						!(args[0] & TMM_FADE) ? prefadems : 0,
+						!(args[0] & TMM_FADE) ? postfadems : 0);
 
-					if ((line->args[0] & TMM_FADE) && fadetarget)
+					if ((args[0] & TMM_FADE) && fadetarget)
 					{
 						if (!postfadems)
 							S_SetInternalMusicVolume(fadetarget);
@@ -2531,16 +2786,16 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 			break;
 
 		case 414: // Play SFX
-			P_PlaySFX(line->stringargs[0] ? get_number(line->stringargs[0]) : sfx_None, mo, callsec, line->args[2], line->args[0], line->args[1]);
+			P_PlaySFX(stringargs[0] ? get_number(stringargs[0]) : sfx_None, mo, callsec, args[2], args[0], args[1]);
 			break;
 
 		case 415: // Run a script
 			if (cv_runscripts.value)
 			{
-				lumpnum_t lumpnum = W_CheckNumForName(line->stringargs[0]);
+				lumpnum_t lumpnum = W_CheckNumForName(stringargs[0]);
 
 				if (lumpnum == LUMPERROR || W_LumpLength(lumpnum) == 0)
-					CONS_Debug(DBG_SETUP, "Line type 415 Executor: script lump %s not found/not valid.\n", line->stringargs[0]);
+					CONS_Debug(DBG_SETUP, "Line type 415 Executor: script lump %s not found/not valid.\n", stringargs[0]);
 				else
 				{
 					void *lump = W_CacheLumpNum(lumpnum, PU_CACHE);
@@ -2555,30 +2810,30 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 			break;
 
 		case 416: // Spawn adjustable fire flicker
-			TAG_ITER_SECTORS(line->args[0], secnum)
-				P_SpawnAdjustableFireFlicker(&sectors[secnum], line->args[2],
-					line->args[3] ? sectors[secnum].lightlevel : line->args[4], line->args[1]);
+			TAG_ITER_SECTORS(args[0], secnum)
+				P_SpawnAdjustableFireFlicker(&sectors[secnum], args[2],
+					args[3] ? sectors[secnum].lightlevel : args[4], args[1]);
 			break;
 
 		case 417: // Spawn adjustable glowing light
-			TAG_ITER_SECTORS(line->args[0], secnum)
-				P_SpawnAdjustableGlowingLight(&sectors[secnum], line->args[2],
-					line->args[3] ? sectors[secnum].lightlevel : line->args[4], line->args[1]);
+			TAG_ITER_SECTORS(args[0], secnum)
+				P_SpawnAdjustableGlowingLight(&sectors[secnum], args[2],
+					args[3] ? sectors[secnum].lightlevel : args[4], args[1]);
 			break;
 
 		case 418: // Spawn adjustable strobe flash
-			TAG_ITER_SECTORS(line->args[0], secnum)
-				P_SpawnAdjustableStrobeFlash(&sectors[secnum], line->args[3],
-					(line->args[4] & TMB_USETARGET) ? sectors[secnum].lightlevel : line->args[5],
-					line->args[1], line->args[2], line->args[4] & TMB_SYNC);
+			TAG_ITER_SECTORS(args[0], secnum)
+				P_SpawnAdjustableStrobeFlash(&sectors[secnum], args[3],
+					(args[4] & TMB_USETARGET) ? sectors[secnum].lightlevel : args[5],
+					args[1], args[2], args[4] & TMB_SYNC);
 			break;
 
 		case 420: // Fade light levels in tagged sectors to new value
-			P_FadeLight(line->args[0], line->args[1], line->args[2], line->args[3] & TMF_TICBASED, line->args[3] & TMF_OVERRIDE, line->args[3] & TMF_RELATIVE);
+			P_FadeLight(args[0], args[1], args[2], args[3] & TMF_TICBASED, args[3] & TMF_OVERRIDE, args[3] & TMF_RELATIVE);
 			break;
 
 		case 421: // Stop lighting effect in tagged sectors
-			TAG_ITER_SECTORS(line->args[0], secnum)
+			TAG_ITER_SECTORS(args[0], secnum)
 				if (sectors[secnum].lightingdata)
 				{
 					P_RemoveThinker(&((thinkerdata_t *)sectors[secnum].lightingdata)->thinker);
@@ -2592,11 +2847,11 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 				INT32 aim;
 
 				if ((!mo || !mo->player) && !titlemapinaction) // only players have views, and title screens
-					return;
+					return false;
 
-				altview = P_FindObjectTypeFromTag(MT_ALTVIEWMAN, line->args[0]);
+				altview = P_FindObjectTypeFromTag(MT_ALTVIEWMAN, args[0]);
 				if (!altview || !altview->spawnpoint)
-					return;
+					return false;
 
 				// If titlemap, set the camera ref for title's thinker
 				// This is not revoked until overwritten; awayviewtics is ignored
@@ -2611,7 +2866,7 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 						P_ResetCamera(mo->player, &camera2);  // reset p2 camera on p2 getting an awayviewmobj
 				}
 
-				aim = udmf ? altview->spawnpoint->pitch : line->args[2];
+				aim = udmf ? altview->spawnpoint->pitch : args[2];
 				aim = (aim + 360) % 360;
 				aim *= (ANGLE_90>>8);
 				aim /= 90;
@@ -2620,30 +2875,30 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 					titlemapcameraref->cusval = (angle_t)aim;
 				else {
 					mo->player->awayviewaiming = (angle_t)aim;
-					mo->player->awayviewtics = line->args[1];
+					mo->player->awayviewtics = args[1];
 				}
 			}
 			break;
 
 		case 423: // Change Sky
-			if ((mo && mo->player && P_IsLocalPlayer(mo->player)) || line->args[1])
-				P_SetupLevelSky(line->args[0], line->args[1]);
+			if ((mo && mo->player && P_IsLocalPlayer(mo->player)) || args[1])
+				P_SetupLevelSky(args[0], args[1]);
 			break;
 
 		case 424: // Change Weather
-			if (line->args[1])
+			if (args[1])
 			{
-				globalweather = (UINT8)(line->args[0]);
+				globalweather = (UINT8)(args[0]);
 				P_SwitchWeather(globalweather);
 			}
 			else if (mo && mo->player && P_IsLocalPlayer(mo->player))
-				P_SwitchWeather(line->args[0]);
+				P_SwitchWeather(args[0]);
 			break;
 
 		case 425: // Calls P_SetMobjState on calling mobj
 			if (mo && !mo->player)
 			{
-				statenum_t state = line->stringargs[0] ? get_number(line->stringargs[0]) : S_NULL;
+				statenum_t state = stringargs[0] ? get_number(stringargs[0]) : S_NULL;
 				if (state >= 0 && state < NUMSTATES)
 					P_SetMobjState(mo, state);
 			}
@@ -2651,9 +2906,9 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 
 		case 426: // Moves the mobj to its sector's soundorg and on the floor, and stops it
 			if (!mo)
-				return;
+				return false;
 
-			if (line->args[0])
+			if (args[0])
 			{
 				P_UnsetThingPosition(mo);
 				mo->x = mo->subsector->sector->soundorg.x;
@@ -2674,7 +2929,7 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 
 				// Reset bot too.
 				if (bot) {
-					if (line->args[0])
+					if (args[0])
 						P_SetOrigin(bot, mo->x, mo->y, mo->z);
 					bot->momx = bot->momy = bot->momz = 1;
 					bot->pmomz = 0;
@@ -2688,26 +2943,26 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 
 		case 427: // Awards points if the mobj is a player
 			if (mo && mo->player)
-				P_AddPlayerScore(mo->player, line->args[0]);
+				P_AddPlayerScore(mo->player, args[0]);
 			break;
 
 		case 428: // Start floating platform movement
-			EV_DoElevator(line->args[0], line, elevateContinuous);
+			EV_DoElevator(args[0], line, elevateContinuous);
 			break;
 
 		case 429: // Crush planes once
-			if (line->args[1] == TMP_FLOOR)
-				EV_DoFloor(line->args[0], line, crushFloorOnce);
-			else if (line->args[1] == TMP_CEILING)
-				EV_DoCrush(line->args[0], line, crushCeilOnce);
+			if (args[1] == TMP_FLOOR)
+				EV_DoFloor(args[0], line, crushFloorOnce);
+			else if (args[1] == TMP_CEILING)
+				EV_DoCrush(args[0], line, crushCeilOnce);
 			else
-				EV_DoCrush(line->args[0], line, crushBothOnce);
+				EV_DoCrush(args[0], line, crushBothOnce);
 			break;
 
 		case 432: // Enable/Disable 2D Mode
 			if (mo && mo->player)
 			{
-				if (line->args[0])
+				if (args[0])
 					mo->flags2 &= ~MF2_TWOD;
 				else
 					mo->flags2 |= MF2_TWOD;
@@ -2722,13 +2977,13 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 			break;
 
 		case 433: // Flip/flop gravity. Works on pushables, too!
-			if (line->args[1])
+			if (args[1])
 				mo->flags2 ^= MF2_OBJECTFLIP;
-			else if (line->args[0])
+			else if (args[0])
 				mo->flags2 &= ~MF2_OBJECTFLIP;
 			else
 				mo->flags2 |= MF2_OBJECTFLIP;
-			if (line->args[2])
+			if (args[2])
 				mo->eflags |= MFE_VERTICALFLIP;
 			if (bot)
 				bot->flags2 = (bot->flags2 & ~MF2_OBJECTFLIP) | (mo->flags2 & MF2_OBJECTFLIP);
@@ -2737,8 +2992,8 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 		case 434: // Custom Power
 			if (mo && mo->player)
 			{
-				powertype_t power = line->stringargs[0] ? get_number(line->stringargs[0]) : 0;
-				INT32 value = line->stringargs[1] ? get_number(line->stringargs[1]) : 0;
+				powertype_t power = stringargs[0] ? get_number(stringargs[0]) : 0;
+				INT32 value = stringargs[1] ? get_number(stringargs[1]) : 0;
 				if (value == -1) // 'Infinite'
 					value = UINT16_MAX;
 
@@ -2755,7 +3010,7 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 				thinker_t *th;
 
 				fixed_t length = R_PointToDist2(line->v2->x, line->v2->y, line->v1->x, line->v1->y);
-				fixed_t speed = line->args[1] << FRACBITS;
+				fixed_t speed = args[1] << FRACBITS;
 				fixed_t dx = FixedMul(FixedMul(FixedDiv(line->dx, length), speed) >> SCROLL_SHIFT, CARRYFACTOR);
 				fixed_t dy = FixedMul(FixedMul(FixedDiv(line->dy, length), speed) >> SCROLL_SHIFT, CARRYFACTOR);
 
@@ -2765,7 +3020,7 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 						continue;
 
 					scroller = (scroll_t *)th;
-					if (!Tag_Find(&sectors[scroller->affectee].tags, line->args[0]))
+					if (!Tag_Find(&sectors[scroller->affectee].tags, args[0]))
 						continue;
 
 					scroller->dx = dx;
@@ -2776,8 +3031,8 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 
 		case 436: // Shatter block remotely
 			{
-				INT16 sectag = (INT16)(line->args[0]);
-				INT16 foftag = (INT16)(line->args[1]);
+				INT16 sectag = (INT16)(args[0]);
+				INT16 foftag = (INT16)(args[1]);
 				sector_t *sec; // Sector that the FOF is visible in
 				ffloor_t *rover; // FOF that we are going to crumble
 				boolean foundrover = false; // for debug, "Can't find a FOF" message
@@ -2789,7 +3044,7 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 					if (!sec->ffloors)
 					{
 						CONS_Debug(DBG_GAMELOGIC, "Line type 436 Executor: Target sector #%d has no FOFs.\n", secnum);
-						return;
+						return false;
 					}
 
 					for (rover = sec->ffloors; rover; rover = rover->next)
@@ -2805,7 +3060,7 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 					if (!foundrover)
 					{
 						CONS_Debug(DBG_GAMELOGIC, "Line type 436 Executor: Can't find a FOF control sector with tag %d\n", foftag);
-						return;
+						return false;
 					}
 				}
 			}
@@ -2814,10 +3069,10 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 		case 437: // Disable Player Controls
 			if (mo && mo->player)
 			{
-				UINT16 fractime = (UINT16)(line->args[0]);
+				UINT16 fractime = (UINT16)(args[0]);
 				if (fractime < 1)
 					fractime = 1; //instantly wears off upon leaving
-				if (line->args[1])
+				if (args[1])
 					fractime |= 1<<15; //more crazy &ing, as if music stuff wasn't enough
 				mo->player->powers[pw_nocontrol] = fractime;
 				if (bot)
@@ -2828,7 +3083,7 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 		case 438: // Set player scale
 			if (mo)
 			{
-				mo->destscale = FixedDiv(line->args[0]<<FRACBITS, 100<<FRACBITS);
+				mo->destscale = FixedDiv(args[0]<<FRACBITS, 100<<FRACBITS);
 				if (mo->destscale < FRACUNIT/100)
 					mo->destscale = FRACUNIT/100;
 				if (mo->player && bot)
@@ -2840,20 +3095,20 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 			{
 				size_t linenum;
 				side_t *setfront = &sides[line->sidenum[0]];
-				side_t *setback = (line->args[3] && line->sidenum[1] != NO_SIDEDEF) ? &sides[line->sidenum[1]] : setfront;
+				side_t *setback = (args[3] && line->sidenum[1] != NO_SIDEDEF) ? &sides[line->sidenum[1]] : setfront;
 				side_t *this;
-				boolean always = !(line->args[2]); // If args[2] is set: Only change mid texture if mid texture already exists on tagged lines, etc.
+				boolean always = !(args[2]); // If args[2] is set: Only change mid texture if mid texture already exists on tagged lines, etc.
 
 				for (linenum = 0; linenum < numlines; linenum++)
 				{
 					if (lines[linenum].special == 439)
 						continue; // Don't override other set texture lines!
 
-					if (!Tag_Find(&lines[linenum].tags, line->args[0]))
+					if (!Tag_Find(&lines[linenum].tags, args[0]))
 						continue; // Find tagged lines
 
 					// Front side
-					if (line->args[1] != TMSD_BACK)
+					if (args[1] != TMSD_BACK)
 					{
 						this = &sides[lines[linenum].sidenum[0]];
 						if (always || this->toptexture) this->toptexture = setfront->toptexture;
@@ -2862,7 +3117,7 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 					}
 
 					// Back side
-					if (line->args[1] != TMSD_FRONT && lines[linenum].sidenum[1] != NO_SIDEDEF)
+					if (args[1] != TMSD_FRONT && lines[linenum].sidenum[1] != NO_SIDEDEF)
 					{
 						this = &sides[lines[linenum].sidenum[1]];
 						if (always || this->toptexture) this->toptexture = setback->toptexture;
@@ -2880,7 +3135,7 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 
 		case 441: // Trigger unlockable
 			{
-				INT32 trigid = line->args[0];
+				INT32 trigid = args[0];
 
 				if (trigid < 0 || trigid > 31) // limited by 32 bit variable
 					CONS_Debug(DBG_GAMELOGIC, "Unlockable trigger (sidedef %u): bad trigger ID %d\n", line->sidenum[0], trigid);
@@ -2905,22 +3160,22 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 
 		case 442: // Calls P_SetMobjState on mobjs of a given type in the tagged sectors
 		{
-			const mobjtype_t type = line->stringargs[0] ? get_number(line->stringargs[0]) : MT_NULL;
+			const mobjtype_t type = stringargs[0] ? get_number(stringargs[0]) : MT_NULL;
 			statenum_t state = NUMSTATES;
 			mobj_t *thing;
 
 			if (type < 0 || type >= NUMMOBJTYPES)
 				break;
 
-			if (!line->args[1])
+			if (!args[1])
 			{
-				state = line->stringargs[1] ? get_number(line->stringargs[1]) : S_NULL;
+				state = stringargs[1] ? get_number(stringargs[1]) : S_NULL;
 
 				if (state < 0 || state >= NUMSTATES)
 					break;
 			}
 
-			TAG_ITER_SECTORS(line->args[0], secnum)
+			TAG_ITER_SECTORS(args[0], secnum)
 			{
 				boolean tryagain;
 				do {
@@ -2930,7 +3185,7 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 						if (thing->type != type)
 							continue;
 
-						if (!P_SetMobjState(thing, line->args[1] ? thing->state->nextstate : state))
+						if (!P_SetMobjState(thing, args[1] ? thing->state->nextstate : state))
 						{ // mobj was removed
 							tryagain = true; // snext is corrupt, we'll have to start over.
 							break;
@@ -2942,7 +3197,7 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 		}
 
 		case 443: // Calls a named Lua function
-			if (line->stringargs[0])
+			if (stringargs[0])
 				LUA_HookLinedefExecute(line, mo, callsec);
 			else
 				CONS_Alert(CONS_WARNING, "Linedef %s is missing the hook name of the Lua function to call! (This should be given in stringarg0)\n", sizeu1(line-lines));
@@ -2950,9 +3205,9 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 
 		case 444: // Earthquake camera
 		{
-			quake.intensity = line->args[1] << FRACBITS;
-			quake.radius = line->args[2] << FRACBITS;
-			quake.time = line->args[0];
+			quake.intensity = args[1] << FRACBITS;
+			quake.radius = args[2] << FRACBITS;
+			quake.time = args[0];
 
 			quake.epicenter = NULL; /// \todo
 
@@ -2966,8 +3221,8 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 
 		case 445: // Force block disappear remotely (reappear if args[2] is set)
 			{
-				INT16 sectag = (INT16)(line->args[0]);
-				INT16 foftag = (INT16)(line->args[1]);
+				INT16 sectag = (INT16)(args[0]);
+				INT16 foftag = (INT16)(args[1]);
 				sector_t *sec; // Sector that the FOF is visible (or not visible) in
 				ffloor_t *rover; // FOF to vanish/un-vanish
 				boolean foundrover = false; // for debug, "Can't find a FOF" message
@@ -2980,7 +3235,7 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 					if (!sec->ffloors)
 					{
 						CONS_Debug(DBG_GAMELOGIC, "Line type 445 Executor: Target sector #%d has no FOFs.\n", secnum);
-						return;
+						return false;
 					}
 
 					for (rover = sec->ffloors; rover; rover = rover->next)
@@ -2992,7 +3247,7 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 							oldflags = rover->fofflags;
 
 							// Abracadabra!
-							if (line->args[2])
+							if (args[2])
 								rover->fofflags |= FOF_EXISTS;
 							else
 								rover->fofflags &= ~FOF_EXISTS;
@@ -3009,7 +3264,7 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 					if (!foundrover)
 					{
 						CONS_Debug(DBG_GAMELOGIC, "Line type 445 Executor: Can't find a FOF control sector with tag %d\n", foftag);
-						return;
+						return false;
 					}
 				}
 			}
@@ -3017,8 +3272,8 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 
 		case 446: // Make block fall remotely (acts like FOF_CRUMBLE)
 			{
-				INT16 sectag = (INT16)(line->args[0]);
-				INT16 foftag = (INT16)(line->args[1]);
+				INT16 sectag = (INT16)(args[0]);
+				INT16 foftag = (INT16)(args[1]);
 				sector_t *sec; // Sector that the FOF is visible in
 				ffloor_t *rover; // FOF that we are going to make fall down
 				boolean foundrover = false; // for debug, "Can't find a FOF" message
@@ -3028,7 +3283,7 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 				if (mo) // NULL check
 					player = mo->player;
 
-				if (line->args[2] & TMFR_NORETURN) // don't respawn!
+				if (args[2] & TMFR_NORETURN) // don't respawn!
 					respawn = false;
 
 				TAG_ITER_SECTORS(sectag, secnum)
@@ -3038,7 +3293,7 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 					if (!sec->ffloors)
 					{
 						CONS_Debug(DBG_GAMELOGIC, "Line type 446 Executor: Target sector #%d has no FOFs.\n", secnum);
-						return;
+						return false;
 					}
 
 					for (rover = sec->ffloors; rover; rover = rover->next)
@@ -3047,8 +3302,8 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 						{
 							foundrover = true;
 
-							if (line->args[2] & TMFR_CHECKFLAG) // FOF flags determine respawn ability instead?
-								respawn = !(rover->fofflags & FOF_NORETURN) ^ !!(line->args[2] & TMFR_NORETURN); // TMFR_NORETURN inverts
+							if (args[2] & TMFR_CHECKFLAG) // FOF flags determine respawn ability instead?
+								respawn = !(rover->fofflags & FOF_NORETURN) ^ !!(args[2] & TMFR_NORETURN); // TMFR_NORETURN inverts
 
 							EV_StartCrumble(rover->master->frontsector, rover, (rover->fofflags & FOF_FLOATBOB), player, rover->alpha, respawn);
 						}
@@ -3057,7 +3312,7 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 					if (!foundrover)
 					{
 						CONS_Debug(DBG_GAMELOGIC, "Line type 446 Executor: Can't find a FOF control sector with tag %d\n", foftag);
-						return;
+						return false;
 					}
 				}
 			}
@@ -3074,27 +3329,27 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 				source = sides[line->sidenum[0]].colormap_data;
 			else
 			{
-				if (!line->args[1])
+				if (!args[1])
 					source = line->frontsector->extra_colormap;
 				else
 				{
-					INT32 sourcesec = Tag_Iterate_Sectors(line->args[1], 0);
+					INT32 sourcesec = Tag_Iterate_Sectors(args[1], 0);
 					if (sourcesec == -1)
 					{
-						CONS_Debug(DBG_GAMELOGIC, "Line type 447 Executor: Can't find sector with source colormap (tag %d)!\n", line->args[1]);
-						return;
+						CONS_Debug(DBG_GAMELOGIC, "Line type 447 Executor: Can't find sector with source colormap (tag %d)!\n", args[1]);
+						return false;
 					}
 					source = sectors[sourcesec].extra_colormap;
 				}
 			}
-			TAG_ITER_SECTORS(line->args[0], secnum)
+			TAG_ITER_SECTORS(args[0], secnum)
 			{
 				if (sectors[secnum].colormap_protected)
 					continue;
 
 				P_ResetColormapFader(&sectors[secnum]);
 
-				if (line->args[2] & TMCF_RELATIVE)
+				if (args[2] & TMCF_RELATIVE)
 				{
 					extracolormap_t *target = (!udmf && (line->flags & ML_TFERLINE) && line->sidenum[1] != NO_SIDEDEF) ?
 						sides[line->sidenum[1]].colormap_data : sectors[secnum].extra_colormap; // use back colormap instead of target sector
@@ -3102,17 +3357,17 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 						extracolormap_t *exc = R_AddColormaps(
 							target,
 							source,
-							line->args[2] & TMCF_SUBLIGHTR,
-							line->args[2] & TMCF_SUBLIGHTG,
-							line->args[2] & TMCF_SUBLIGHTB,
-							line->args[2] & TMCF_SUBLIGHTA,
-							line->args[2] & TMCF_SUBFADER,
-							line->args[2] & TMCF_SUBFADEG,
-							line->args[2] & TMCF_SUBFADEB,
-							line->args[2] & TMCF_SUBFADEA,
-							line->args[2] & TMCF_SUBFADESTART,
-							line->args[2] & TMCF_SUBFADEEND,
-							line->args[2] & TMCF_IGNOREFLAGS,
+							args[2] & TMCF_SUBLIGHTR,
+							args[2] & TMCF_SUBLIGHTG,
+							args[2] & TMCF_SUBLIGHTB,
+							args[2] & TMCF_SUBLIGHTA,
+							args[2] & TMCF_SUBFADER,
+							args[2] & TMCF_SUBFADEG,
+							args[2] & TMCF_SUBFADEB,
+							args[2] & TMCF_SUBFADEA,
+							args[2] & TMCF_SUBFADESTART,
+							args[2] & TMCF_SUBFADEEND,
+							args[2] & TMCF_IGNOREFLAGS,
 							false);
 
 					if (!(sectors[secnum].extra_colormap = R_GetColormapFromList(exc)))
@@ -3130,13 +3385,13 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 			break;
 		}
 		case 448: // Change skybox viewpoint/centerpoint
-			if ((mo && mo->player && P_IsLocalPlayer(mo->player)) || line->args[3])
+			if ((mo && mo->player && P_IsLocalPlayer(mo->player)) || args[3])
 			{
-				INT32 viewid = line->args[0];
-				INT32 centerid = line->args[1];
+				INT32 viewid = args[0];
+				INT32 centerid = args[1];
 
 				// set viewpoint mobj
-				if (line->args[2] != TMS_CENTERPOINT)
+				if (args[2] != TMS_CENTERPOINT)
 				{
 					if (viewid >= 0 && viewid < 16)
 						skyboxmo[0] = skyboxviewpnts[viewid];
@@ -3145,7 +3400,7 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 				}
 
 				// set centerpoint mobj
-				if (line->args[2] != TMS_VIEWPOINT)
+				if (args[2] != TMS_VIEWPOINT)
 				{
 					if (centerid >= 0 && centerid < 16)
 						skyboxmo[1] = skyboxcenterpnts[centerid];
@@ -3156,14 +3411,14 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 				CONS_Debug(DBG_GAMELOGIC, "Line type 448 Executor: viewid = %d, centerid = %d, viewpoint? = %s, centerpoint? = %s\n",
 						viewid,
 						centerid,
-						((line->args[2] == TMS_CENTERPOINT) ? "no" : "yes"),
-						((line->args[2] == TMS_VIEWPOINT) ? "no" : "yes"));
+						((args[2] == TMS_CENTERPOINT) ? "no" : "yes"),
+						((args[2] == TMS_VIEWPOINT) ? "no" : "yes"));
 			}
 			break;
 
 		case 449: // Enable bosses with parameter
 		{
-			INT32 bossid = line->args[0];
+			INT32 bossid = args[0];
 			if (bossid & ~15) // if any bits other than first 16 are set
 			{
 				CONS_Alert(CONS_WARNING,
@@ -3171,7 +3426,7 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 					bossid);
 				break;
 			}
-			if (line->args[1])
+			if (args[1])
 			{
 				bossdisabled |= (1<<bossid);
 				CONS_Debug(DBG_GAMELOGIC, "Line type 449 Executor: bossid disabled = %d", bossid);
@@ -3185,13 +3440,13 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 		}
 
 		case 450: // Execute Linedef Executor - for recursion
-			P_LinedefExecute(line->args[0], mo, NULL);
+			P_LinedefExecute(args[0], mo, NULL);
 			break;
 
 		case 451: // Execute Random Linedef Executor
 		{
-			INT32 rvalue1 = line->args[0];
-			INT32 rvalue2 = line->args[1];
+			INT32 rvalue1 = args[0];
+			INT32 rvalue2 = args[1];
 			INT32 result;
 
 			if (rvalue1 <= rvalue2)
@@ -3205,9 +3460,9 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 
 		case 452: // Set FOF alpha
 		{
-			INT16 destvalue = (INT16)(line->args[2]);
-			INT16 sectag = (INT16)(line->args[0]);
-			INT16 foftag = (INT16)(line->args[1]);
+			INT16 destvalue = (INT16)(args[2]);
+			INT16 sectag = (INT16)(args[0]);
+			INT16 foftag = (INT16)(args[1]);
 			sector_t *sec; // Sector that the FOF is visible in
 			ffloor_t *rover; // FOF that we are going to operate
 			boolean foundrover = false; // for debug, "Can't find a FOF" message
@@ -3219,7 +3474,7 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 				if (!sec->ffloors)
 				{
 					CONS_Debug(DBG_GAMELOGIC, "Line type 452 Executor: Target sector #%d has no FOFs.\n", secnum);
-					return;
+					return false;
 				}
 
 				for (rover = sec->ffloors; rover; rover = rover->next)
@@ -3230,7 +3485,7 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 
 						// If fading an invisible FOF whose render flags we did not yet set,
 						// initialize its alpha to 0 for relative alpha calculation
-						if (!(line->args[3] & TMST_DONTDOTRANSLUCENT) &&      // do translucent
+						if (!(args[3] & TMST_DONTDOTRANSLUCENT) &&      // do translucent
 							(rover->spawnflags & FOF_NOSHADE) && // do not include light blocks, which don't set FOF_NOSHADE
 							!(rover->spawnflags & FOF_RENDERSIDES) &&
 							!(rover->spawnflags & FOF_RENDERPLANES) &&
@@ -3240,11 +3495,11 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 						P_RemoveFakeFloorFader(rover);
 						P_FadeFakeFloor(rover,
 							rover->alpha,
-							max(0, min(255, (line->args[3] & TMST_RELATIVE) ? rover->alpha + destvalue : destvalue)),
+							max(0, min(255, (args[3] & TMST_RELATIVE) ? rover->alpha + destvalue : destvalue)),
 							0,                                         // set alpha immediately
 							false, NULL,                               // tic-based logic
 							false,                                     // do not handle FOF_EXISTS
-							!(line->args[3] & TMST_DONTDOTRANSLUCENT), // handle FOF_TRANSLUCENT
+							!(args[3] & TMST_DONTDOTRANSLUCENT), // handle FOF_TRANSLUCENT
 							false,                                     // do not handle lighting
 							false,                                     // do not handle colormap
 							false,                                     // do not handle collision
@@ -3256,7 +3511,7 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 				if (!foundrover)
 				{
 					CONS_Debug(DBG_GAMELOGIC, "Line type 452 Executor: Can't find a FOF control sector with tag %d\n", foftag);
-					return;
+					return false;
 				}
 			}
 			break;
@@ -3264,10 +3519,10 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 
 		case 453: // Fade FOF
 		{
-			INT16 destvalue = (INT16)(line->args[2]);
-			INT16 speed = (INT16)(line->args[3]);
-			INT16 sectag = (INT16)(line->args[0]);
-			INT16 foftag = (INT16)(line->args[1]);
+			INT16 destvalue = (INT16)(args[2]);
+			INT16 speed = (INT16)(args[3]);
+			INT16 sectag = (INT16)(args[0]);
+			INT16 foftag = (INT16)(args[1]);
 			sector_t *sec; // Sector that the FOF is visible in
 			ffloor_t *rover; // FOF that we are going to operate
 			boolean foundrover = false; // for debug, "Can't find a FOF" message
@@ -3280,7 +3535,7 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 				if (!sec->ffloors)
 				{
 					CONS_Debug(DBG_GAMELOGIC, "Line type 453 Executor: Target sector #%d has no FOFs.\n", secnum);
-					return;
+					return false;
 				}
 
 				for (rover = sec->ffloors; rover; rover = rover->next)
@@ -3290,7 +3545,7 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 						foundrover = true;
 
 						// Prevent continuous execs from interfering on an existing fade
-						if (!(line->args[4] & TMFT_OVERRIDE)
+						if (!(args[4] & TMFT_OVERRIDE)
 							&& rover->fadingdata)
 							//&& ((fade_t*)rover->fadingdata)->timer > (ticbased ? 2 : speed*2))
 						{
@@ -3302,20 +3557,20 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 							P_AddFakeFloorFader(rover, secnum, j,
 								destvalue,
 								speed,
-								(line->args[4] & TMFT_TICBASED),           // tic-based logic
-								(line->args[4] & TMFT_RELATIVE),           // Relative destvalue
-								!(line->args[4] & TMFT_DONTDOEXISTS),      // do not handle FOF_EXISTS
-								!(line->args[4] & TMFT_DONTDOTRANSLUCENT), // do not handle FOF_TRANSLUCENT
-								!(line->args[4] & TMFT_DONTDOLIGHTING),    // do not handle lighting
-								!(line->args[4] & TMFT_DONTDOCOLORMAP),    // do not handle colormap
-								!(line->args[4] & TMFT_IGNORECOLLISION),   // do not handle collision
-								(line->args[4] & TMFT_GHOSTFADE),          // do ghost fade (no collision during fade)
-								(line->args[4] & TMFT_USEEXACTALPHA));     // use exact alpha values (for opengl)
+								(args[4] & TMFT_TICBASED),           // tic-based logic
+								(args[4] & TMFT_RELATIVE),           // Relative destvalue
+								!(args[4] & TMFT_DONTDOEXISTS),      // do not handle FOF_EXISTS
+								!(args[4] & TMFT_DONTDOTRANSLUCENT), // do not handle FOF_TRANSLUCENT
+								!(args[4] & TMFT_DONTDOLIGHTING),    // do not handle lighting
+								!(args[4] & TMFT_DONTDOCOLORMAP),    // do not handle colormap
+								!(args[4] & TMFT_IGNORECOLLISION),   // do not handle collision
+								(args[4] & TMFT_GHOSTFADE),          // do ghost fade (no collision during fade)
+								(args[4] & TMFT_USEEXACTALPHA));     // use exact alpha values (for opengl)
 						else
 						{
 							// If fading an invisible FOF whose render flags we did not yet set,
 							// initialize its alpha to 1 for relative alpha calculation
-							if (!(line->args[4] & TMFT_DONTDOTRANSLUCENT) &&      // do translucent
+							if (!(args[4] & TMFT_DONTDOTRANSLUCENT) &&      // do translucent
 								(rover->spawnflags & FOF_NOSHADE) && // do not include light blocks, which don't set FOF_NOSHADE
 								!(rover->spawnflags & FOF_RENDERSIDES) &&
 								!(rover->spawnflags & FOF_RENDERPLANES) &&
@@ -3325,16 +3580,16 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 							P_RemoveFakeFloorFader(rover);
 							P_FadeFakeFloor(rover,
 								rover->alpha,
-								max(0, min(255, (line->args[4] & TMFT_RELATIVE) ? rover->alpha + destvalue : destvalue)),
+								max(0, min(255, (args[4] & TMFT_RELATIVE) ? rover->alpha + destvalue : destvalue)),
 								0,                                         // set alpha immediately
 								false, NULL,                               // tic-based logic
-								!(line->args[4] & TMFT_DONTDOEXISTS),      // do not handle FOF_EXISTS
-								!(line->args[4] & TMFT_DONTDOTRANSLUCENT), // do not handle FOF_TRANSLUCENT
-								!(line->args[4] & TMFT_DONTDOLIGHTING),    // do not handle lighting
-								!(line->args[4] & TMFT_DONTDOCOLORMAP),    // do not handle colormap
-								!(line->args[4] & TMFT_IGNORECOLLISION),   // do not handle collision
-								(line->args[4] & TMFT_GHOSTFADE),          // do ghost fade (no collision during fade)
-								(line->args[4] & TMFT_USEEXACTALPHA));     // use exact alpha values (for opengl)
+								!(args[4] & TMFT_DONTDOEXISTS),      // do not handle FOF_EXISTS
+								!(args[4] & TMFT_DONTDOTRANSLUCENT), // do not handle FOF_TRANSLUCENT
+								!(args[4] & TMFT_DONTDOLIGHTING),    // do not handle lighting
+								!(args[4] & TMFT_DONTDOCOLORMAP),    // do not handle colormap
+								!(args[4] & TMFT_IGNORECOLLISION),   // do not handle collision
+								(args[4] & TMFT_GHOSTFADE),          // do ghost fade (no collision during fade)
+								(args[4] & TMFT_USEEXACTALPHA));     // use exact alpha values (for opengl)
 						}
 					}
 					j++;
@@ -3343,7 +3598,7 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 				if (!foundrover)
 				{
 					CONS_Debug(DBG_GAMELOGIC, "Line type 453 Executor: Can't find a FOF control sector with tag %d\n", foftag);
-					return;
+					return false;
 				}
 			}
 			break;
@@ -3351,8 +3606,8 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 
 		case 454: // Stop fading FOF
 		{
-			INT16 sectag = (INT16)(line->args[0]);
-			INT16 foftag = (INT16)(line->args[1]);
+			INT16 sectag = (INT16)(args[0]);
+			INT16 foftag = (INT16)(args[1]);
 			sector_t *sec; // Sector that the FOF is visible in
 			ffloor_t *rover; // FOF that we are going to operate
 			boolean foundrover = false; // for debug, "Can't find a FOF" message
@@ -3364,7 +3619,7 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 				if (!sec->ffloors)
 				{
 					CONS_Debug(DBG_GAMELOGIC, "Line type 454 Executor: Target sector #%d has no FOFs.\n", secnum);
-					return;
+					return false;
 				}
 
 				for (rover = sec->ffloors; rover; rover = rover->next)
@@ -3374,14 +3629,14 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 						foundrover = true;
 
 						P_ResetFakeFloorFader(rover, NULL,
-							!(line->args[2])); // do not finalize collision flags
+							!(args[2])); // do not finalize collision flags
 					}
 				}
 
 				if (!foundrover)
 				{
 					CONS_Debug(DBG_GAMELOGIC, "Line type 454 Executor: Can't find a FOF control sector with tag %d\n", foftag);
-					return;
+					return false;
 				}
 			}
 			break;
@@ -3394,21 +3649,21 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 				dest = sides[line->sidenum[0]].colormap_data;
 			else
 			{
-				if (!line->args[1])
+				if (!args[1])
 					dest = line->frontsector->extra_colormap;
 				else
 				{
-					INT32 destsec = Tag_Iterate_Sectors(line->args[1], 0);
+					INT32 destsec = Tag_Iterate_Sectors(args[1], 0);
 					if (destsec == -1)
 					{
-						CONS_Debug(DBG_GAMELOGIC, "Line type 455 Executor: Can't find sector with destination colormap (tag %d)!\n", line->args[1]);
-						return;
+						CONS_Debug(DBG_GAMELOGIC, "Line type 455 Executor: Can't find sector with destination colormap (tag %d)!\n", args[1]);
+						return false;
 					}
 					dest = sectors[destsec].extra_colormap;
 				}
 			}
 
-			TAG_ITER_SECTORS(line->args[0], secnum)
+			TAG_ITER_SECTORS(args[0], secnum)
 			{
 				extracolormap_t *source_exc, *dest_exc, *exc;
 
@@ -3416,7 +3671,7 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 					continue;
 
 				// Don't interrupt ongoing fade
-				if (!(line->args[3] & TMCF_OVERRIDE)
+				if (!(args[3] & TMCF_OVERRIDE)
 					&& sectors[secnum].fadecolormapdata)
 					//&& ((fadecolormap_t*)sectors[secnum].fadecolormapdata)->timer > (ticbased ? 2 : speed*2))
 				{
@@ -3430,7 +3685,7 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 
 				exc = sectors[secnum].extra_colormap;
 
-				if (!(line->args[3] & TMCF_FROMBLACK) // Override fade from default rgba
+				if (!(args[3] & TMCF_FROMBLACK) // Override fade from default rgba
 					&& !R_CheckDefaultColormap(dest, true, false, false)
 					&& R_CheckDefaultColormap(exc, true, false, false))
 				{
@@ -3452,22 +3707,22 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 				else
 					source_exc = exc ? exc : R_GetDefaultColormap();
 
-				if (line->args[3] & TMCF_RELATIVE)
+				if (args[3] & TMCF_RELATIVE)
 				{
 					exc = R_AddColormaps(
 						source_exc,
 						dest,
-						line->args[3] & TMCF_SUBLIGHTR,
-						line->args[3] & TMCF_SUBLIGHTG,
-						line->args[3] & TMCF_SUBLIGHTB,
-						line->args[3] & TMCF_SUBLIGHTA,
-						line->args[3] & TMCF_SUBFADER,
-						line->args[3] & TMCF_SUBFADEG,
-						line->args[3] & TMCF_SUBFADEB,
-						line->args[3] & TMCF_SUBFADEA,
-						line->args[3] & TMCF_SUBFADESTART,
-						line->args[3] & TMCF_SUBFADEEND,
-						line->args[3] & TMCF_IGNOREFLAGS,
+						args[3] & TMCF_SUBLIGHTR,
+						args[3] & TMCF_SUBLIGHTG,
+						args[3] & TMCF_SUBLIGHTB,
+						args[3] & TMCF_SUBLIGHTA,
+						args[3] & TMCF_SUBFADER,
+						args[3] & TMCF_SUBFADEG,
+						args[3] & TMCF_SUBFADEB,
+						args[3] & TMCF_SUBFADEA,
+						args[3] & TMCF_SUBFADESTART,
+						args[3] & TMCF_SUBFADEEND,
+						args[3] & TMCF_IGNOREFLAGS,
 						false);
 				}
 				else
@@ -3483,27 +3738,27 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 					Z_Free(exc);
 
 				Add_ColormapFader(&sectors[secnum], source_exc, dest_exc, true, // tic-based timing
-					line->args[2]);
+					args[2]);
 			}
 			break;
 		}
 		case 456: // Stop fade colormap
-			TAG_ITER_SECTORS(line->args[0], secnum)
+			TAG_ITER_SECTORS(args[0], secnum)
 				P_ResetColormapFader(&sectors[secnum]);
 			break;
 
 		case 457: // Track mobj angle to point
 			if (mo)
 			{
-				INT32 failureangle = FixedAngle((min(max(abs(line->args[1]), 0), 360))*FRACUNIT);
-				INT32 failuredelay = abs(line->args[2]);
-				INT32 failureexectag = line->args[3];
-				boolean persist = !!(line->args[4]);
+				INT32 failureangle = FixedAngle((min(max(abs(args[1]), 0), 360))*FRACUNIT);
+				INT32 failuredelay = abs(args[2]);
+				INT32 failureexectag = args[3];
+				boolean persist = !!(args[4]);
 				mobj_t *anchormo;
 
-				anchormo = P_FindObjectTypeFromTag(MT_ANGLEMAN, line->args[0]);
+				anchormo = P_FindObjectTypeFromTag(MT_ANGLEMAN, args[0]);
 				if (!anchormo)
-					return;
+					return false;
 
 				mo->eflags |= MFE_TRACERANGLE;
 				P_SetTarget(&mo->tracer, anchormo);
@@ -3527,24 +3782,24 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 			// console player only
 			if (mo && mo->player && P_IsLocalPlayer(mo->player) && (!bot || bot != mo))
 			{
-				INT32 promptnum = max(0, line->args[0] - 1);
-				INT32 pagenum = max(0, line->args[1] - 1);
-				INT32 postexectag = abs(line->args[3]);
-
-				boolean closetextprompt = (line->args[2] & TMP_CLOSE);
-				//boolean allplayers = (line->args[2] & TMP_ALLPLAYERS);
-				boolean runpostexec = (line->args[2] & TMP_RUNPOSTEXEC);
-				boolean blockcontrols = !(line->args[2] & TMP_KEEPCONTROLS);
-				boolean freezerealtime = !(line->args[2] & TMP_KEEPREALTIME);
-				//boolean freezethinkers = (line->args[2] & TMP_FREEZETHINKERS);
-				boolean callbynamedtag = (line->args[2] & TMP_CALLBYNAME);
+				INT32 promptnum = max(0, args[0] - 1);
+				INT32 pagenum = max(0, args[1] - 1);
+				INT32 postexectag = abs(args[3]);
+
+				boolean closetextprompt = (args[2] & TMP_CLOSE);
+				//boolean allplayers = (args[2] & TMP_ALLPLAYERS);
+				boolean runpostexec = (args[2] & TMP_RUNPOSTEXEC);
+				boolean blockcontrols = !(args[2] & TMP_KEEPCONTROLS);
+				boolean freezerealtime = !(args[2] & TMP_KEEPREALTIME);
+				//boolean freezethinkers = (args[2] & TMP_FREEZETHINKERS);
+				boolean callbynamedtag = (args[2] & TMP_CALLBYNAME);
 
 				if (closetextprompt)
 					F_EndTextPrompt(false, false);
 				else
 				{
-					if (callbynamedtag && line->stringargs[0] && line->stringargs[0][0])
-						F_GetPromptPageByNamedTag(line->stringargs[0], &promptnum, &pagenum);
+					if (callbynamedtag && stringargs[0] && stringargs[0][0])
+						F_GetPromptPageByNamedTag(stringargs[0], &promptnum, &pagenum);
 					F_StartTextPrompt(promptnum, pagenum, mo, runpostexec ? postexectag : 0, blockcontrols, freezerealtime);
 				}
 			}
@@ -3552,8 +3807,8 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 
 		case 460: // Award rings
 			{
-				INT16 rings = line->args[0];
-				INT32 delay = line->args[1];
+				INT16 rings = args[0];
+				INT32 delay = args[1];
 				if (mo && mo->player)
 				{
 					if (delay <= 0 || !(leveltime % delay))
@@ -3564,28 +3819,28 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 
 		case 461: // Spawns an object on the map based on texture offsets
 			{
-				const mobjtype_t type = line->stringargs[0] ? get_number(line->stringargs[0]) : MT_NULL;
+				const mobjtype_t type = stringargs[0] ? get_number(stringargs[0]) : MT_NULL;
 				mobj_t *mobj;
 
 				fixed_t x, y, z;
 
-				if (line->args[4]) // If args[4] is set, spawn randomly within a range
+				if (args[4]) // If args[4] is set, spawn randomly within a range
 				{
-					x = P_RandomRange(line->args[0], line->args[5])<<FRACBITS;
-					y = P_RandomRange(line->args[1], line->args[6])<<FRACBITS;
-					z = P_RandomRange(line->args[2], line->args[7])<<FRACBITS;
+					x = P_RandomRange(args[0], args[5])<<FRACBITS;
+					y = P_RandomRange(args[1], args[6])<<FRACBITS;
+					z = P_RandomRange(args[2], args[7])<<FRACBITS;
 				}
 				else
 				{
-					x = line->args[0] << FRACBITS;
-					y = line->args[1] << FRACBITS;
-					z = line->args[2] << FRACBITS;
+					x = args[0] << FRACBITS;
+					y = args[1] << FRACBITS;
+					z = args[2] << FRACBITS;
 				}
 
 				mobj = P_SpawnMobj(x, y, z, type);
 				if (mobj)
 				{
-					mobj->angle = FixedAngle(line->args[3] << FRACBITS);
+					mobj->angle = FixedAngle(args[3] << FRACBITS);
 					CONS_Debug(DBG_GAMELOGIC, "Linedef Type %d - Spawn Object: %d spawned at (%d, %d, %d)\n", line->special, mobj->type, mobj->x>>FRACBITS, mobj->y>>FRACBITS, mobj->z>>FRACBITS); //TODO: Convert mobj->type to a string somehow.
 				}
 				else
@@ -3615,10 +3870,10 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 			{
 				if (mo)
 				{
-					INT32 color = line->stringargs[0] ? get_number(line->stringargs[0]) : SKINCOLOR_NONE;
+					INT32 color = stringargs[0] ? get_number(stringargs[0]) : SKINCOLOR_NONE;
 
 					if (color < 0 || color >= numskincolors)
-						return;
+						return false;
 
 					var1 = 0;
 					var2 = color;
@@ -3634,7 +3889,7 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 
 				// Find the center of the Eggtrap and release all the pretty animals!
 				// The chimps are my friends.. heeheeheheehehee..... - LouisJM
-				TAG_ITER_THINGS(line->args[0], mtnum)
+				TAG_ITER_THINGS(args[0], mtnum)
 				{
 					mo2 = mapthings[mtnum].mobj;
 
@@ -3650,7 +3905,7 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 					P_KillMobj(mo2, NULL, mo, 0);
 				}
 
-				if (!(line->args[1]))
+				if (!(args[1]))
 				{
 					INT32 i;
 
@@ -3672,19 +3927,19 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 				if (!udmf)
 					break;
 
-				TAG_ITER_LINES(line->args[0], linenum)
+				TAG_ITER_LINES(args[0], linenum)
 				{
-					if (line->args[2])
-						lines[linenum].executordelay += line->args[1];
+					if (args[2])
+						lines[linenum].executordelay += args[1];
 					else
-						lines[linenum].executordelay = line->args[1];
+						lines[linenum].executordelay = args[1];
 				}
 			}
 			break;
 
 		case 466: // Set level failure state
 			{
-				if (line->args[1])
+				if (args[1])
 				{
 					stagefailed = false;
 					CONS_Debug(DBG_GAMELOGIC, "Stage can be completed successfully!\n");
@@ -3698,7 +3953,7 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 			break;
 
 		case 467: // Set light level
-			TAG_ITER_SECTORS(line->args[0], secnum)
+			TAG_ITER_SECTORS(args[0], secnum)
 			{
 				if (sectors[secnum].lightingdata)
 				{
@@ -3707,26 +3962,26 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 					sectors[secnum].lightingdata = NULL;
 				}
 
-				if (line->args[2] == TML_FLOOR)
+				if (args[2] == TML_FLOOR)
 				{
-					if (line->args[3])
-						sectors[secnum].floorlightlevel += line->args[1];
+					if (args[3])
+						sectors[secnum].floorlightlevel += args[1];
 					else
-						sectors[secnum].floorlightlevel = line->args[1];
+						sectors[secnum].floorlightlevel = args[1];
 				}
-				else if (line->args[2] == TML_CEILING)
+				else if (args[2] == TML_CEILING)
 				{
-					if (line->args[3])
-						sectors[secnum].ceilinglightlevel += line->args[1];
+					if (args[3])
+						sectors[secnum].ceilinglightlevel += args[1];
 					else
-						sectors[secnum].ceilinglightlevel = line->args[1];
+						sectors[secnum].ceilinglightlevel = args[1];
 				}
 				else
 				{
-					if (line->args[3])
-						sectors[secnum].lightlevel += line->args[1];
+					if (args[3])
+						sectors[secnum].lightlevel += args[1];
 					else
-						sectors[secnum].lightlevel = line->args[1];
+						sectors[secnum].lightlevel = args[1];
 					sectors[secnum].lightlevel = max(0, min(255, sectors[secnum].lightlevel));
 				}
 			}
@@ -3739,18 +3994,18 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 			if (!udmf)
 				break;
 
-			if (line->args[1] < 0 || line->args[1] >= NUMLINEARGS)
+			if (args[1] < 0 || args[1] >= NUM_SCRIPT_ARGS)
 			{
-				CONS_Debug(DBG_GAMELOGIC, "Linedef type 468: Invalid linedef arg %d\n", line->args[1]);
+				CONS_Debug(DBG_GAMELOGIC, "Linedef type 468: Invalid linedef arg %d\n", args[1]);
 				break;
 			}
 
-			TAG_ITER_LINES(line->args[0], linenum)
+			TAG_ITER_LINES(args[0], linenum)
 			{
-				if (line->args[3])
-					lines[linenum].args[line->args[1]] += line->args[2];
+				if (args[3])
+					lines[linenum].args[args[1]] += args[2];
 				else
-					lines[linenum].args[line->args[1]] = line->args[2];
+					lines[linenum].args[args[1]] = args[2];
 			}
 		}
 		break;
@@ -3762,29 +4017,70 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 			if (!udmf)
 				break;
 
-			if (!line->stringargs[0])
+			if (!stringargs[0])
 				break;
 
-			gravityvalue = FloatToFixed(atof(line->stringargs[0]));
+			gravityvalue = FloatToFixed(atof(stringargs[0]));
 
-			TAG_ITER_SECTORS(line->args[0], secnum)
+			TAG_ITER_SECTORS(args[0], secnum)
 			{
-				if (line->args[1])
+				if (args[1])
 					sectors[secnum].gravity = FixedMul(sectors[secnum].gravity, gravityvalue);
 				else
 					sectors[secnum].gravity = gravityvalue;
 
-				if (line->args[2] == TMF_ADD)
+				if (args[2] == TMF_ADD)
 					sectors[secnum].flags |= MSF_GRAVITYFLIP;
-				else if (line->args[2] == TMF_REMOVE)
+				else if (args[2] == TMF_REMOVE)
 					sectors[secnum].flags &= ~MSF_GRAVITYFLIP;
 
-				if (line->args[3])
+				if (args[3])
 					sectors[secnum].specialflags |= SSF_GRAVITYOVERRIDE;
 			}
 		}
 		break;
 
+		case 475: // ACS_Execute
+			{
+				if (!stringargs[0])
+				{
+					CONS_Debug(DBG_GAMELOGIC, "Linedef type 475: No script name given\n");
+					return false;
+				}
+
+				ACS_Execute(stringargs[0], &args[1], NUM_SCRIPT_ARGS - 1, (const char* const*)&stringargs[1], NUM_SCRIPT_STRINGARGS - 1, activator);
+			}
+			break;
+		case 476: // ACS_ExecuteAlways
+			{
+				if (!stringargs[0])
+				{
+					CONS_Debug(DBG_GAMELOGIC, "Linedef type 476: No script name given\n");
+					return false;
+				}
+
+				ACS_ExecuteAlways(stringargs[0], &args[1], NUM_SCRIPT_ARGS - 1, (const char* const*)&stringargs[1], NUM_SCRIPT_STRINGARGS - 1, activator);
+			}
+			break;
+		case 477: // ACS_Suspend
+			if (!stringargs[0])
+			{
+				CONS_Debug(DBG_GAMELOGIC, "Linedef type 477: No script name given\n");
+				return false;
+			}
+
+			ACS_Suspend(stringargs[0]);
+			break;
+		case 478: // ACS_Terminate
+			if (!stringargs[0])
+			{
+				CONS_Debug(DBG_GAMELOGIC, "Linedef type 478: No script name given\n");
+				return false;
+			}
+
+			ACS_Terminate(stringargs[0]);
+			break;
+
 		case 480: // Polyobj_DoorSlide
 		case 481: // Polyobj_DoorSwing
 			PolyDoor(line);
@@ -3811,6 +4107,8 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 		default:
 			break;
 	}
+
+	return true;
 }
 
 //
@@ -5328,6 +5626,328 @@ void P_CheckMobjTrigger(mobj_t *mobj, boolean pushable)
 	P_CheckMobjSectorTrigger(mobj, originalsector);
 }
 
+static void P_SectorActionWasActivated(sector_t *sec)
+{
+	if ((sec->activation & SECSPAC_TRIGGERMASK) == SECSPAC_ONCESPECIAL)
+	{
+		sec->action = 0;
+	}
+}
+
+static boolean P_SectorActionIsContinuous(sector_t *sec)
+{
+	return ((sec->activation & SECSPAC_TRIGGERMASK) == SECSPAC_CONTINUOUSSPECIAL);
+}
+
+static boolean P_AllowSpecialEnter(sector_t *sec, mobj_t *thing)
+{
+	if (thing->player != NULL)
+	{
+		return !!(sec->activation & SECSPAC_ENTER);
+	}
+	else if ((thing->flags & (MF_ENEMY|MF_BOSS)) != 0)
+	{
+		return !!(sec->activation & SECSPAC_ENTERMONSTER);
+	}
+	else if (thing->flags & MF_MISSILE)
+	{
+		return !!(sec->activation & SECSPAC_ENTERMISSILE);
+	}
+
+	// No activation flags for you.
+	return false;
+}
+
+static boolean P_AllowSpecialFloor(sector_t *sec, mobj_t *thing)
+{
+	if (thing->player != NULL)
+	{
+		return !!(sec->activation & SECSPAC_FLOOR);
+	}
+	else if ((thing->flags & (MF_ENEMY|MF_BOSS)) != 0)
+	{
+		return !!(sec->activation & SECSPAC_FLOORMONSTER);
+	}
+	else if (thing->flags & MF_MISSILE)
+	{
+		return !!(sec->activation & SECSPAC_FLOORMISSILE);
+	}
+
+	// No activation flags for you.
+	return false;
+}
+
+static boolean P_AllowSpecialCeiling(sector_t *sec, mobj_t *thing)
+{
+	if (thing->player != NULL)
+	{
+		return !!(sec->activation & SECSPAC_CEILING);
+	}
+	else if ((thing->flags & (MF_ENEMY|MF_BOSS)) != 0)
+	{
+		return !!(sec->activation & SECSPAC_CEILINGMONSTER);
+	}
+	else if (thing->flags & MF_MISSILE)
+	{
+		return !!(sec->activation & SECSPAC_CEILINGMISSILE);
+	}
+
+	// No activation flags for you.
+	return false;
+}
+
+static void P_CheckMobj3DFloorAction(mobj_t *mo, sector_t *sec, boolean continuous, boolean sectorchanged)
+{
+	sector_t *originalsector = mo->subsector->sector;
+	ffloor_t *rover;
+	sector_t *roversec;
+
+	activator_t *activator = NULL;
+	boolean result = false;
+
+	for (rover = sec->ffloors; rover; rover = rover->next)
+	{
+		fixed_t top = INT32_MIN;
+		fixed_t bottom = INT32_MAX;
+		fixed_t mid = 0;
+
+		roversec = rover->master->frontsector;
+
+		if (P_SectorActionIsContinuous(roversec) != continuous)
+		{
+			// Does not match continuous state.
+			continue;
+		}
+
+		if (P_CanActivateSpecial(roversec->action) == false)
+		{
+			// No special to even activate.
+			continue;
+		}
+
+		top = P_GetSpecialTopZ(mo, roversec, sec);
+		bottom = P_GetSpecialBottomZ(mo, roversec, sec);
+		mid = bottom + ((top - bottom) / 2);
+
+		if (mo->z > top || mo->z + mo->height < bottom)
+		{
+			// Out of bounds.
+			continue;
+		}
+
+		if (P_AllowSpecialEnter(roversec, mo) == false)
+		{
+			boolean floor = false;
+			boolean ceiling = false;
+
+			if (P_AllowSpecialFloor(roversec, mo) == true)
+			{
+				floor = (P_GetMobjFeet(mo) >= mid);
+			}
+
+			if (P_AllowSpecialCeiling(roversec, mo) == true)
+			{
+				ceiling = (P_GetMobjHead(mo) <= mid);
+			}
+
+			if (floor == false && ceiling == false)
+			{
+				continue;
+			}
+		}
+		else if (sectorchanged == false)
+		{
+			continue;
+		}
+
+		activator = Z_Calloc(sizeof(activator_t), PU_LEVEL, NULL);
+		I_Assert(activator != NULL);
+
+		P_SetTarget(&activator->mo, mo);
+		activator->sector = roversec;
+
+		result = P_ProcessSpecial(activator, roversec->action, roversec->args, roversec->stringargs);
+
+		P_SetTarget(&activator->mo, NULL);
+		Z_Free(activator);
+
+		if (result == true)
+		{
+			P_SectorActionWasActivated(roversec);
+		}
+
+		if TELEPORTED(mo) return;
+	}
+}
+
+static void P_CheckMobjPolyobjAction(mobj_t *mo, boolean continuous, boolean sectorchanged)
+{
+	sector_t *originalsector = mo->subsector->sector;
+	polyobj_t *po;
+	sector_t *polysec;
+	boolean touching = false;
+	boolean inside = false;
+
+	activator_t *activator = NULL;
+	boolean result = false;
+
+	for (po = mo->subsector->polyList; po; po = (polyobj_t *)(po->link.next))
+	{
+		polysec = po->lines[0]->backsector;
+
+		if (P_SectorActionIsContinuous(polysec) != continuous)
+		{
+			// Does not match continuous state.
+			continue;
+		}
+
+		if (P_CanActivateSpecial(polysec->action) == false)
+		{
+			// No special to even activate.
+			continue;
+		}
+
+		touching = P_MobjTouchingPolyobj(po, mo);
+		inside = P_MobjInsidePolyobj(po, mo);
+
+		if (!(inside || touching))
+		{
+			continue;
+		}
+
+		if (P_AllowSpecialEnter(polysec, mo) == false)
+		{
+			boolean floor = false;
+			boolean ceiling = false;
+
+			if (P_AllowSpecialFloor(polysec, mo) == true)
+			{
+				floor = (P_GetMobjFeet(mo) == P_GetSpecialTopZ(mo, polysec, polysec));
+			}
+
+			if (P_AllowSpecialCeiling(polysec, mo) == true)
+			{
+				ceiling = (P_GetMobjHead(mo) == P_GetSpecialBottomZ(mo, polysec, polysec));
+			}
+
+			if (floor == false && ceiling == false)
+			{
+				continue;
+			}
+		}
+		else if (sectorchanged == false)
+		{
+			continue;
+		}
+
+		activator = Z_Calloc(sizeof(activator_t), PU_LEVEL, NULL);
+		I_Assert(activator != NULL);
+
+		P_SetTarget(&activator->mo, mo);
+		activator->sector = polysec;
+
+		result = P_ProcessSpecial(activator, polysec->action, polysec->args, polysec->stringargs);
+
+		P_SetTarget(&activator->mo, NULL);
+		Z_Free(activator);
+
+		if (result == true)
+		{
+			P_SectorActionWasActivated(polysec);
+		}
+
+		if TELEPORTED(mo) return;
+	}
+}
+
+static void P_CheckMobjSectorAction(mobj_t *mo, sector_t *sec, boolean continuous, boolean sectorchanged)
+{
+	activator_t *activator = NULL;
+	boolean result = false;
+
+	if (P_SectorActionIsContinuous(sec) != continuous)
+	{
+		// Does not match continuous state.
+		return;
+	}
+
+	if (P_CanActivateSpecial(sec->action) == false)
+	{
+		// No special to even activate.
+		return;
+	}
+
+	if (P_AllowSpecialEnter(sec, mo) == false)
+	{
+		boolean floor = false;
+		boolean ceiling = false;
+
+		if (P_AllowSpecialFloor(sec, mo) == true)
+		{
+			floor = (P_GetMobjFeet(mo) == P_GetSpecialBottomZ(mo, sec, sec));
+		}
+
+		if (P_AllowSpecialCeiling(sec, mo) == true)
+		{
+			ceiling = (P_GetMobjHead(mo) == P_GetSpecialTopZ(mo, sec, sec));
+		}
+
+		if (floor == false && ceiling == false)
+		{
+			return;
+		}
+	}
+	else if (sectorchanged == false)
+	{
+		return;
+	}
+
+	activator = Z_Calloc(sizeof(activator_t), PU_LEVEL, NULL);
+	I_Assert(activator != NULL);
+
+	P_SetTarget(&activator->mo, mo);
+	activator->sector = sec;
+
+	result = P_ProcessSpecial(activator, sec->action, sec->args, sec->stringargs);
+
+	P_SetTarget(&activator->mo, NULL);
+	Z_Free(activator);
+
+	if (result == true)
+	{
+		P_SectorActionWasActivated(sec);
+	}
+}
+
+void P_CheckMobjTouchingSectorActions(mobj_t *mobj, boolean continuous, boolean sectorchanged)
+{
+	sector_t *originalsector;
+
+	if (mobj->subsector == NULL)
+	{
+		return;
+	}
+
+	originalsector = mobj->subsector->sector;
+
+	if (mobj->player != NULL)
+	{
+		if (mobj->player->spectator == true)
+		{
+			// Ignore spectators.
+			return;
+		}
+	}
+
+	P_CheckMobj3DFloorAction(mobj, originalsector, continuous, sectorchanged);
+	if TELEPORTED(mobj)	return;
+
+	P_CheckMobjPolyobjAction(mobj, continuous, sectorchanged);
+	if TELEPORTED(mobj)	return;
+
+	P_CheckMobjSectorAction(mobj, originalsector, continuous, sectorchanged);
+}
+
 #undef TELEPORTED
 
 /** Animate planes, scroll walls, etc. and keeps track of level timelimit and exits if time is up.
@@ -8945,3 +9565,21 @@ static void P_SpawnPushers(void)
 		}
 	}
 }
+
+void P_CheckSectorTransitionalEffects(mobj_t *thing, sector_t *prevsec, boolean wasgrounded)
+{
+	if (!udmf)
+	{
+		return;
+	}
+
+	boolean sectorchanged = (prevsec != thing->subsector->sector);
+
+	if (!sectorchanged && wasgrounded == P_IsObjectOnGround(thing))
+	{
+		return;
+	}
+
+	// Check for each time / once sector special actions
+	P_CheckMobjTouchingSectorActions(thing, false, sectorchanged);
+}
diff --git a/src/p_spec.h b/src/p_spec.h
index b9c3a2ca3c2055481b54cd2d2fce3217df244fe7..365738e885b838f115c388e26cd39103476128e8 100644
--- a/src/p_spec.h
+++ b/src/p_spec.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -17,6 +17,12 @@
 #ifndef __P_SPEC__
 #define __P_SPEC__
 
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include "r_defs.h"
+
 extern mobj_t *skyboxmo[2]; // current skybox mobjs: 0 = viewpoint, 1 = centerpoint
 extern mobj_t *skyboxviewpnts[16]; // array of MT_SKYBOX viewpoint mobjs
 extern mobj_t *skyboxcenterpnts[16]; // array of MT_SKYBOX centerpoint mobjs
@@ -517,10 +523,12 @@ sector_t *P_PlayerTouchingSectorSpecial(player_t *player, INT32 section, INT32 n
 sector_t *P_PlayerTouchingSectorSpecialFlag(player_t *player, sectorspecialflags_t flag);
 void P_PlayerInSpecialSector(player_t *player);
 void P_CheckMobjTrigger(mobj_t *mobj, boolean pushable);
+void P_CheckMobjTouchingSectorActions(mobj_t *mobj, boolean continuous, boolean sectorchanged);
 sector_t *P_FindPlayerTrigger(player_t *player, line_t *sourceline);
 boolean P_IsPlayerValid(size_t playernum);
 boolean P_CanPlayerTrigger(size_t playernum);
 void P_ProcessSpecialSector(player_t *player, sector_t *sector, sector_t *roversector);
+void P_CheckSectorTransitionalEffects(mobj_t *thing, sector_t *prevsec, boolean wasgrounded);
 
 fixed_t P_FindLowestFloorSurrounding(sector_t *sec);
 fixed_t P_FindHighestFloorSurrounding(sector_t *sec);
@@ -533,6 +541,28 @@ fixed_t P_FindHighestCeilingSurrounding(sector_t *sec);
 
 INT32 P_FindMinSurroundingLight(sector_t *sector, INT32 max);
 
+void P_CrossSpecialLine(line_t *line, INT32 side, mobj_t *thing);
+void P_PushSpecialLine(line_t *line, mobj_t *thing);
+void P_ActivateThingSpecial(mobj_t *mo, mobj_t *source);
+
+mobj_t* P_FindObjectTypeFromTag(mobjtype_t type, mtag_t tag);
+
+//
+// Special activation info
+//
+typedef struct
+{
+	mobj_t *mo;
+	line_t *line;
+	UINT8 side;
+	sector_t *sector;
+	polyobj_t *po;
+	boolean fromLineSpecial; // Backwards compat for ACS
+} activator_t;
+
+boolean P_CanActivateSpecial(INT16 special);
+boolean P_ProcessSpecial(activator_t *activator, INT16 special, INT32 *args, char **stringargs);
+
 void P_SetupSignExit(player_t *player);
 boolean P_IsFlagAtBase(mobjtype_t flag);
 
@@ -1117,4 +1147,8 @@ void T_PlaneDisplace(planedisplace_t *pd);
 
 void P_CalcHeight(player_t *player);
 
+#ifdef __cplusplus
+} // extern "C"
+#endif
+
 #endif
diff --git a/src/p_tick.c b/src/p_tick.c
index 56e0fd897bfbba718885a1104224d1554790f227..d792c7b8c505e2ebefeff715c02056dff79214a0 100644
--- a/src/p_tick.c
+++ b/src/p_tick.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -21,8 +21,9 @@
 #include "m_random.h"
 #include "lua_script.h"
 #include "lua_hook.h"
+#include "acs/interface.h"
 #include "m_perfstats.h"
-#include "i_system.h" // I_GetPreciseTime
+#include "i_system.h"
 #include "r_main.h"
 #include "r_fps.h"
 #include "i_video.h" // rendermode
@@ -38,6 +39,8 @@
 
 tic_t leveltime;
 
+UINT32 thinker_era = 0;
+
 //
 // THINKERS
 // All thinkers should be allocated by Z_Calloc
@@ -200,10 +203,22 @@ void Command_CountMobjs_f(void)
 void P_InitThinkers(void)
 {
 	UINT8 i;
+
+	P_InvalidateThinkersWithoutInit();
+
 	for (i = 0; i < NUM_THINKERLISTS; i++)
 		thlist[i].prev = thlist[i].next = &thlist[i];
 }
 
+//
+// P_InvalidateThinkersWithoutInit
+//
+
+void P_InvalidateThinkersWithoutInit(void)
+{
+	thinker_era++;
+}
+
 // Adds a new thinker at the end of the list.
 void P_AddThinker(const thinklistnum_t n, thinker_t *thinker)
 {
@@ -443,6 +458,9 @@ static inline void P_RunThinkers(void)
 		PS_STOP_TIMING(ps_thlist_times[i]);
 	}
 
+	PS_START_TIMING(ps_acs_time);
+	ACS_Tick();
+	PS_STOP_TIMING(ps_acs_time);
 }
 
 //
diff --git a/src/p_tick.h b/src/p_tick.h
index bbc227e081433652a9a3d79b8ab0513c531677bc..93e2249ba01f90ee14e6100349baf3bf6f75ac1b 100644
--- a/src/p_tick.h
+++ b/src/p_tick.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -16,8 +16,8 @@
 
 #include "doomdef.h"
 
-#ifdef __GNUG__
-#pragma interface
+#ifdef __cplusplus
+extern "C" {
 #endif
 
 extern tic_t leveltime;
@@ -31,6 +31,8 @@ void P_PreTicker(INT32 frames);
 void P_DoTeamscrambling(void);
 void P_RemoveThinkerDelayed(thinker_t *thinker); //killed
 
+extern UINT32 thinker_era;
+
 mobj_t *P_SetTarget2(mobj_t **mo, mobj_t *target
 #ifdef PARANOIA
 		, const char *source_file, int source_line
@@ -43,4 +45,8 @@ mobj_t *P_SetTarget2(mobj_t **mo, mobj_t *target
 #define P_SetTarget P_SetTarget2
 #endif
 
+#ifdef __cplusplus
+} // extern "C"
+#endif
+
 #endif
diff --git a/src/p_user.c b/src/p_user.c
index 7cd128cf080792f64df4a56ca4aeb4e9076e6b94..949f3f24453d75c0ea10e79358ffca4b6d74078a 100644
--- a/src/p_user.c
+++ b/src/p_user.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -42,6 +42,7 @@
 #include "st_stuff.h"
 #include "lua_script.h"
 #include "lua_hook.h"
+#include "acs/interface.h"
 #include "b_bot.h"
 // Objectplace
 #include "m_cheat.h"
@@ -968,6 +969,52 @@ pflags_t P_GetJumpFlags(player_t *player)
 	return PF_JUMPED;
 }
 
+UINT8 P_FindLowestLap(void)
+{
+	INT32 i;
+	UINT8 lowest = UINT8_MAX;
+
+	if (!circuitmap)
+		return 0;
+
+	for (i = 0; i < MAXPLAYERS; i++)
+	{
+		if (!playeringame[i] || players[i].spectator)
+			continue;
+
+		if (lowest == UINT8_MAX || players[i].laps < lowest)
+		{
+			lowest = players[i].laps;
+		}
+	}
+
+	CONS_Debug(DBG_GAMELOGIC, "Lowest laps found: %d\n", lowest);
+
+	return lowest;
+}
+
+UINT8 P_FindHighestLap(void)
+{
+	INT32 i;
+	UINT8 highest = 0;
+
+	if (!circuitmap)
+		return 0;
+
+	for (i = 0; i < MAXPLAYERS; i++)
+	{
+		if (!playeringame[i] || players[i].spectator)
+			continue;
+
+		if (players[i].laps > highest)
+			highest = players[i].laps;
+	}
+
+	CONS_Debug(DBG_GAMELOGIC, "Highest laps found: %d\n", highest);
+
+	return highest;
+}
+
 //
 // P_PlayerInPain
 //
@@ -2316,6 +2363,7 @@ void P_DoPlayerExit(player_t *player, boolean finishedflag)
 	player->powers[pw_underwater] = 0;
 	player->powers[pw_spacetime] = 0;
 	P_RestoreMusic(player);
+	ACS_RunPlayerFinishScript(player);
 }
 
 boolean P_InSpaceSector(mobj_t *mo) // Returns true if you are in space
diff --git a/src/r_defs.h b/src/r_defs.h
index da4dd2d70e6049479eacd24c51af11b4a995507b..9fc4527e1be0a228aae33df54345d8f7ceeda82a 100644
--- a/src/r_defs.h
+++ b/src/r_defs.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -409,6 +409,48 @@ typedef enum
 	SSF_NOPHYSICSCEILING = 1<<22,
 } sectorspecialflags_t;
 
+typedef enum
+{
+	// Mask to get trigger type.
+	SECSPAC_TRIGGERMASK			= 0x0000000F,
+
+	// Special action is activated once.
+	SECSPAC_ONCESPECIAL			= 0x00000000,
+
+	// Special action is repeatable.
+	SECSPAC_REPEATSPECIAL		= 0x00000001,
+
+	// Special action is activated continously.
+	SECSPAC_CONTINUOUSSPECIAL	= 0x00000002,
+
+	// When a player enters this sector.
+	SECSPAC_ENTER				= 0x00000010,
+
+	// When a player touches the floor of this sector.
+	SECSPAC_FLOOR				= 0x00000020,
+
+	// When a player touches the ceiling of this sector.
+	SECSPAC_CEILING				= 0x00000040,
+
+	// When an enemy enters this sector.
+	SECSPAC_ENTERMONSTER		= 0x00000080,
+
+	// When an enemy touches the floor of this sector.
+	SECSPAC_FLOORMONSTER		= 0x00000100,
+
+	// When an enemy touches the ceiling of this sector.
+	SECSPAC_CEILINGMONSTER		= 0x00000200,
+
+	// When a projectile enters this sector.
+	SECSPAC_ENTERMISSILE		= 0x00000400,
+
+	// When a projectile touches the floor of this sector.
+	SECSPAC_FLOORMISSILE		= 0x00000800,
+
+	// When a projectile touches the ceiling of this sector.
+	SECSPAC_CEILINGMISSILE		= 0x00001000,
+} sectoractionflags_t;
+
 typedef enum
 {
 	SD_NONE = 0,
@@ -552,6 +594,12 @@ typedef struct sector_s
 	// portals
 	UINT32 portal_floor;
 	UINT32 portal_ceiling;
+
+	// Action specials
+	INT16 action;
+	INT32 args[NUM_SCRIPT_ARGS];
+	char *stringargs[NUM_SCRIPT_STRINGARGS];
+	sectoractionflags_t activation;
 } sector_t;
 
 //
@@ -569,9 +617,6 @@ typedef enum
 
 #define SPECIAL_SECTOR_SETPORTAL 6
 
-#define NUMLINEARGS 10
-#define NUMLINESTRINGARGS 2
-
 #define NO_SIDEDEF 0xFFFFFFFF
 
 typedef struct line_s
@@ -585,10 +630,11 @@ typedef struct line_s
 
 	// Animation related.
 	INT16 flags;
+	UINT32 activation;
 	INT16 special;
 	taglist_t tags;
-	INT32 args[NUMLINEARGS];
-	char *stringargs[NUMLINESTRINGARGS];
+	INT32 args[NUM_SCRIPT_ARGS];
+	char *stringargs[NUM_SCRIPT_STRINGARGS];
 
 	// Visual appearance: sidedefs.
 	UINT32 sidenum[2]; // sidenum[1] will be NO_SIDEDEF if one-sided
@@ -633,6 +679,9 @@ typedef struct
 	// We do not maintain names here.
 	INT32 toptexture, bottomtexture, midtexture;
 
+	// Interpolator installed? (R_CreateInterpolator_SideScroll)
+	boolean acs_interpolated;
+
 	// Linedef the sidedef belongs to
 	line_t *line;
 
@@ -900,7 +949,7 @@ typedef struct
 #endif
 
 // Possible alpha types for a patch.
-enum patchalphastyle {AST_COPY, AST_TRANSLUCENT, AST_ADD, AST_SUBTRACT, AST_REVERSESUBTRACT, AST_MODULATE, AST_OVERLAY, AST_FOG};
+typedef enum patchalphastyle { AST_COPY, AST_TRANSLUCENT, AST_ADD, AST_SUBTRACT, AST_REVERSESUBTRACT, AST_MODULATE, AST_OVERLAY, AST_FOG } patchalphastyle_t;
 
 typedef enum
 {
diff --git a/src/r_fps.h b/src/r_fps.h
index cd40b0a9a572b23b97bb9b2cc493ea6a34a243a7..90677c1d24e8b1ff6d6c84d1aea4a06498d47528 100644
--- a/src/r_fps.h
+++ b/src/r_fps.h
@@ -3,7 +3,7 @@
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
 // Copyright (C) 1999-2000 by Jess Haas, Nicolas Kalkhof, Colin Phipps, Florian Schulze, Andrey Budko (prboom)
-// Copyright (C) 1999-2019 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -20,6 +20,10 @@
 #include "r_state.h"
 #include "m_perfstats.h" // ps_metric_t
 
+#ifdef __cplusplus
+extern "C" {
+#endif
+
 extern consvar_t cv_fpscap;
 
 extern ps_metric_t ps_interp_frac;
@@ -165,4 +169,8 @@ void R_UpdateMobjInterpolators(void);
 void R_ResetMobjInterpolationState(mobj_t *mobj);
 void R_ResetPrecipitationMobjInterpolationState(precipmobj_t *mobj);
 
+#ifdef __cplusplus
+} // extern "C"
+#endif
+
 #endif
diff --git a/src/r_skins.h b/src/r_skins.h
index 1f2c57472d23ffd3026f0370e591dcd18ad77193..9deec2453de5284ba971137a69f6611e20df9723 100644
--- a/src/r_skins.h
+++ b/src/r_skins.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -21,6 +21,10 @@
 #include "r_picformats.h" // spriteinfo_t
 #include "r_defs.h" // spritedef_t
 
+#ifdef __cplusplus
+extern "C" {
+#endif
+
 /// Defaults
 #define SKINNAMESIZE 16
 // should be all lowercase!! S_SKIN processing does a strlwr
@@ -118,4 +122,8 @@ boolean P_IsStateSprite2Super(state_t *state);
 
 void R_RefreshSprite2(void);
 
+#ifdef __cplusplus
+} // extern "C"
+#endif
+
 #endif //__R_SKINS__
diff --git a/src/r_textures.h b/src/r_textures.h
index eb68ec09f21d4fe8d847b048ac12ba9bd3a1817d..e42f02cd0fcc7c95ae8d89a7529688ab6f163a0f 100644
--- a/src/r_textures.h
+++ b/src/r_textures.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -19,6 +19,10 @@
 #include "p_setup.h" // levelflats
 #include "r_data.h"
 
+#ifdef __cplusplus
+extern "C" {
+#endif
+
 #ifdef __GNUG__
 #pragma interface
 #endif
@@ -108,4 +112,8 @@ const char *R_TextureNameForNum(INT32 num);
 
 extern INT32 numtextures;
 
+#ifdef __cplusplus
+} // extern "C"
+#endif
+
 #endif
diff --git a/src/s_sound.h b/src/s_sound.h
index d64778fc37db6fffdf8027ad7aa3cb4f338a6a33..8b7b80fa2dd11c636bf8abb87d76e85abc22e4dd 100644
--- a/src/s_sound.h
+++ b/src/s_sound.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -25,6 +25,10 @@
 extern openmpt_module *openmpt_mhandle;
 #endif
 
+#ifdef __cplusplus
+extern "C" {
+#endif
+
 // mask used to indicate sound origin is player item pickup
 #define PICKUP_SOUND 0x8000
 
@@ -329,4 +333,8 @@ void S_StopSoundByNum(sfxenum_t sfxnum);
 #define S_StartScreamSound S_StartSound
 #endif
 
+#ifdef __cplusplus
+} // extern "C"
+#endif
+
 #endif
diff --git a/src/sdl/CMakeLists.txt b/src/sdl/CMakeLists.txt
index ee48fa2b154568889c50d4e0a6c8d97c5c2a7bf0..99425108e69b3761fd03de7790d02232e44bb898 100644
--- a/src/sdl/CMakeLists.txt
+++ b/src/sdl/CMakeLists.txt
@@ -33,8 +33,6 @@ target_compile_options(SRB2SDL2 PRIVATE
         -Wall
         -Wno-trigraphs
         -W # Was controlled by RELAXWARNINGS
-        -pedantic
-        -Wpedantic
         -Wfloat-equal
         -Wundef
         -Wpointer-arith
diff --git a/src/tables.h b/src/tables.h
index 65d7f72433177f77d711c5e4b5809ed2f9d6e2b8..7a7a9300f50cd908b79e880a1629e5ad63b056ec 100644
--- a/src/tables.h
+++ b/src/tables.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -14,6 +14,10 @@
 #ifndef __TABLES__
 #define __TABLES__
 
+#ifdef __cplusplus
+extern "C" {
+#endif
+
 #ifdef LINUX
 #include <math.h>
 #endif
@@ -120,4 +124,8 @@ matrix_t *FM_RotateZ(matrix_t *dest, angle_t rad);
 #define FINECOSINE(n) (finecosine[n]>>(FINE_FRACBITS-FRACBITS))
 #define FINETANGENT(n) (finetangent[n]>>(FINE_FRACBITS-FRACBITS))
 
+#ifdef __cplusplus
+} // extern "C"
+#endif
+
 #endif
diff --git a/src/taglist.h b/src/taglist.h
index d42a48f05ae056893c34180512901d1300e712e2..7c03549800ee64199495c41c3a953f8898f4f745 100644
--- a/src/taglist.h
+++ b/src/taglist.h
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 // Copyright (C) 2020-2023 by Nev3r.
 //
 // This program is free software distributed under the
@@ -11,11 +11,15 @@
 /// \file  taglist.h
 /// \brief Tag iteration and reading functions and macros' declarations.
 
-#ifndef __R_TAGLIST__
-#define __R_TAGLIST__
+#ifndef __TAGLIST_H__
+#define __TAGLIST_H__
 
 #include "doomtype.h"
 
+#ifdef __cplusplus
+extern "C" {
+#endif
+
 typedef INT16 mtag_t;
 #define MAXTAGS UINT16_MAX
 #define MTAG_GLOBAL -1
@@ -127,4 +131,8 @@ Notes:
 If no elements are found for a given tag, the loop inside won't be executed.
 */
 
-#endif //__R_TAGLIST__
+#ifdef __cplusplus
+} // extern "C"
+#endif
+
+#endif // __TAGLIST_H__
diff --git a/src/tcb_span.hpp b/src/tcb_span.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..fdc3a988a4c2ec5322deee7dc8fc3955f2ccb995
--- /dev/null
+++ b/src/tcb_span.hpp
@@ -0,0 +1,618 @@
+
+/*
+This is an implementation of C++20's std::span
+http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/n4820.pdf
+*/
+
+//          Copyright Tristan Brindle 2018.
+// Distributed under the Boost Software License, Version 1.0.
+//    (See accompanying file ../../LICENSE_1_0.txt or copy at
+//          https://www.boost.org/LICENSE_1_0.txt)
+
+#ifndef TCB_SPAN_HPP_INCLUDED
+#define TCB_SPAN_HPP_INCLUDED
+
+#include <array>
+#include <cstddef>
+#include <cstdint>
+#include <type_traits>
+
+#ifndef TCB_SPAN_NO_EXCEPTIONS
+// Attempt to discover whether we're being compiled with exception support
+#if !(defined(__cpp_exceptions) || defined(__EXCEPTIONS) || defined(_CPPUNWIND))
+#define TCB_SPAN_NO_EXCEPTIONS
+#endif
+#endif
+
+#ifndef TCB_SPAN_NO_EXCEPTIONS
+#include <cstdio>
+#include <stdexcept>
+#endif
+
+// Various feature test macros
+
+#ifndef TCB_SPAN_NAMESPACE_NAME
+#define TCB_SPAN_NAMESPACE_NAME tcb
+#endif
+
+#if __cplusplus >= 201703L || (defined(_MSVC_LANG) && _MSVC_LANG >= 201703L)
+#define TCB_SPAN_HAVE_CPP17
+#endif
+
+#if __cplusplus >= 201402L || (defined(_MSVC_LANG) && _MSVC_LANG >= 201402L)
+#define TCB_SPAN_HAVE_CPP14
+#endif
+
+namespace TCB_SPAN_NAMESPACE_NAME {
+
+// Establish default contract checking behavior
+#if !defined(TCB_SPAN_THROW_ON_CONTRACT_VIOLATION) &&                          \
+    !defined(TCB_SPAN_TERMINATE_ON_CONTRACT_VIOLATION) &&                      \
+    !defined(TCB_SPAN_NO_CONTRACT_CHECKING)
+#if defined(NDEBUG) || !defined(TCB_SPAN_HAVE_CPP14)
+#define TCB_SPAN_NO_CONTRACT_CHECKING
+#else
+#define TCB_SPAN_TERMINATE_ON_CONTRACT_VIOLATION
+#endif
+#endif
+
+#if defined(TCB_SPAN_THROW_ON_CONTRACT_VIOLATION)
+struct contract_violation_error : std::logic_error {
+    explicit contract_violation_error(const char* msg) : std::logic_error(msg)
+    {}
+};
+
+inline void contract_violation(const char* msg)
+{
+    throw contract_violation_error(msg);
+}
+
+#elif defined(TCB_SPAN_TERMINATE_ON_CONTRACT_VIOLATION)
+[[noreturn]] inline void contract_violation(const char* /*unused*/)
+{
+    std::terminate();
+}
+#endif
+
+#if !defined(TCB_SPAN_NO_CONTRACT_CHECKING)
+#define TCB_SPAN_STRINGIFY(cond) #cond
+#define TCB_SPAN_EXPECT(cond)                                                  \
+    cond ? (void) 0 : contract_violation("Expected " TCB_SPAN_STRINGIFY(cond))
+#else
+#define TCB_SPAN_EXPECT(cond)
+#endif
+
+#if defined(TCB_SPAN_HAVE_CPP17) || defined(__cpp_inline_variables)
+#define TCB_SPAN_INLINE_VAR inline
+#else
+#define TCB_SPAN_INLINE_VAR
+#endif
+
+#if defined(TCB_SPAN_HAVE_CPP14) ||                                            \
+    (defined(__cpp_constexpr) && __cpp_constexpr >= 201304)
+#define TCB_SPAN_HAVE_CPP14_CONSTEXPR
+#endif
+
+#if defined(TCB_SPAN_HAVE_CPP14_CONSTEXPR)
+#define TCB_SPAN_CONSTEXPR14 constexpr
+#else
+#define TCB_SPAN_CONSTEXPR14
+#endif
+
+#if defined(TCB_SPAN_HAVE_CPP14_CONSTEXPR) &&                                  \
+    (!defined(_MSC_VER) || _MSC_VER > 1900)
+#define TCB_SPAN_CONSTEXPR_ASSIGN constexpr
+#else
+#define TCB_SPAN_CONSTEXPR_ASSIGN
+#endif
+
+#if defined(TCB_SPAN_NO_CONTRACT_CHECKING)
+#define TCB_SPAN_CONSTEXPR11 constexpr
+#else
+#define TCB_SPAN_CONSTEXPR11 TCB_SPAN_CONSTEXPR14
+#endif
+
+#if defined(TCB_SPAN_HAVE_CPP17) || defined(__cpp_deduction_guides)
+#define TCB_SPAN_HAVE_DEDUCTION_GUIDES
+#endif
+
+#if defined(TCB_SPAN_HAVE_CPP17) || defined(__cpp_lib_byte)
+#define TCB_SPAN_HAVE_STD_BYTE
+#endif
+
+#if defined(TCB_SPAN_HAVE_CPP17) || defined(__cpp_lib_array_constexpr)
+#define TCB_SPAN_HAVE_CONSTEXPR_STD_ARRAY_ETC
+#endif
+
+#if defined(TCB_SPAN_HAVE_CONSTEXPR_STD_ARRAY_ETC)
+#define TCB_SPAN_ARRAY_CONSTEXPR constexpr
+#else
+#define TCB_SPAN_ARRAY_CONSTEXPR
+#endif
+
+#ifdef TCB_SPAN_HAVE_STD_BYTE
+using byte = std::byte;
+#else
+using byte = unsigned char;
+#endif
+
+#if defined(TCB_SPAN_HAVE_CPP17)
+#define TCB_SPAN_NODISCARD [[nodiscard]]
+#else
+#define TCB_SPAN_NODISCARD
+#endif
+
+TCB_SPAN_INLINE_VAR constexpr std::size_t dynamic_extent = SIZE_MAX;
+
+template <typename ElementType, std::size_t Extent = dynamic_extent>
+class span;
+
+namespace detail {
+
+template <typename E, std::size_t S>
+struct span_storage {
+    constexpr span_storage() noexcept = default;
+
+    constexpr span_storage(E* p_ptr, std::size_t /*unused*/) noexcept
+       : ptr(p_ptr)
+    {}
+
+    E* ptr = nullptr;
+    static constexpr std::size_t size = S;
+};
+
+template <typename E>
+struct span_storage<E, dynamic_extent> {
+    constexpr span_storage() noexcept = default;
+
+    constexpr span_storage(E* p_ptr, std::size_t p_size) noexcept
+        : ptr(p_ptr), size(p_size)
+    {}
+
+    E* ptr = nullptr;
+    std::size_t size = 0;
+};
+
+// Reimplementation of C++17 std::size() and std::data()
+#if defined(TCB_SPAN_HAVE_CPP17) ||                                            \
+    defined(__cpp_lib_nonmember_container_access)
+using std::data;
+using std::size;
+#else
+template <class C>
+constexpr auto size(const C& c) -> decltype(c.size())
+{
+    return c.size();
+}
+
+template <class T, std::size_t N>
+constexpr std::size_t size(const T (&)[N]) noexcept
+{
+    return N;
+}
+
+template <class C>
+constexpr auto data(C& c) -> decltype(c.data())
+{
+    return c.data();
+}
+
+template <class C>
+constexpr auto data(const C& c) -> decltype(c.data())
+{
+    return c.data();
+}
+
+template <class T, std::size_t N>
+constexpr T* data(T (&array)[N]) noexcept
+{
+    return array;
+}
+
+template <class E>
+constexpr const E* data(std::initializer_list<E> il) noexcept
+{
+    return il.begin();
+}
+#endif // TCB_SPAN_HAVE_CPP17
+
+#if defined(TCB_SPAN_HAVE_CPP17) || defined(__cpp_lib_void_t)
+using std::void_t;
+#else
+template <typename...>
+using void_t = void;
+#endif
+
+template <typename T>
+using uncvref_t =
+    typename std::remove_cv<typename std::remove_reference<T>::type>::type;
+
+template <typename>
+struct is_span : std::false_type {};
+
+template <typename T, std::size_t S>
+struct is_span<span<T, S>> : std::true_type {};
+
+template <typename>
+struct is_std_array : std::false_type {};
+
+template <typename T, std::size_t N>
+struct is_std_array<std::array<T, N>> : std::true_type {};
+
+template <typename, typename = void>
+struct has_size_and_data : std::false_type {};
+
+template <typename T>
+struct has_size_and_data<T, void_t<decltype(detail::size(std::declval<T>())),
+                                   decltype(detail::data(std::declval<T>()))>>
+    : std::true_type {};
+
+template <typename C, typename U = uncvref_t<C>>
+struct is_container {
+    static constexpr bool value =
+        !is_span<U>::value && !is_std_array<U>::value &&
+        !std::is_array<U>::value && has_size_and_data<C>::value;
+};
+
+template <typename T>
+using remove_pointer_t = typename std::remove_pointer<T>::type;
+
+template <typename, typename, typename = void>
+struct is_container_element_type_compatible : std::false_type {};
+
+template <typename T, typename E>
+struct is_container_element_type_compatible<
+    T, E,
+    typename std::enable_if<
+        !std::is_same<
+            typename std::remove_cv<decltype(detail::data(std::declval<T>()))>::type,
+            void>::value &&
+        std::is_convertible<
+            remove_pointer_t<decltype(detail::data(std::declval<T>()))> (*)[],
+            E (*)[]>::value
+        >::type>
+    : std::true_type {};
+
+template <typename, typename = size_t>
+struct is_complete : std::false_type {};
+
+template <typename T>
+struct is_complete<T, decltype(sizeof(T))> : std::true_type {};
+
+} // namespace detail
+
+template <typename ElementType, std::size_t Extent>
+class span {
+    static_assert(std::is_object<ElementType>::value,
+                  "A span's ElementType must be an object type (not a "
+                  "reference type or void)");
+    static_assert(detail::is_complete<ElementType>::value,
+                  "A span's ElementType must be a complete type (not a forward "
+                  "declaration)");
+    static_assert(!std::is_abstract<ElementType>::value,
+                  "A span's ElementType cannot be an abstract class type");
+
+    using storage_type = detail::span_storage<ElementType, Extent>;
+
+public:
+    // constants and types
+    using element_type = ElementType;
+    using value_type = typename std::remove_cv<ElementType>::type;
+    using size_type = std::size_t;
+    using difference_type = std::ptrdiff_t;
+    using pointer = element_type*;
+    using const_pointer = const element_type*;
+    using reference = element_type&;
+    using const_reference = const element_type&;
+    using iterator = pointer;
+    using reverse_iterator = std::reverse_iterator<iterator>;
+
+    static constexpr size_type extent = Extent;
+
+    // [span.cons], span constructors, copy, assignment, and destructor
+    template <
+        std::size_t E = Extent,
+        typename std::enable_if<(E == dynamic_extent || E <= 0), int>::type = 0>
+    constexpr span() noexcept
+    {}
+
+    TCB_SPAN_CONSTEXPR11 span(pointer ptr, size_type count)
+        : storage_(ptr, count)
+    {
+        TCB_SPAN_EXPECT(extent == dynamic_extent || count == extent);
+    }
+
+    TCB_SPAN_CONSTEXPR11 span(pointer first_elem, pointer last_elem)
+        : storage_(first_elem, last_elem - first_elem)
+    {
+        TCB_SPAN_EXPECT(extent == dynamic_extent ||
+                        last_elem - first_elem ==
+                            static_cast<std::ptrdiff_t>(extent));
+    }
+
+    template <std::size_t N, std::size_t E = Extent,
+              typename std::enable_if<
+                  (E == dynamic_extent || N == E) &&
+                      detail::is_container_element_type_compatible<
+                          element_type (&)[N], ElementType>::value,
+                  int>::type = 0>
+    constexpr span(element_type (&arr)[N]) noexcept : storage_(arr, N)
+    {}
+
+    template <typename T, std::size_t N, std::size_t E = Extent,
+              typename std::enable_if<
+                  (E == dynamic_extent || N == E) &&
+                      detail::is_container_element_type_compatible<
+                          std::array<T, N>&, ElementType>::value,
+                  int>::type = 0>
+    TCB_SPAN_ARRAY_CONSTEXPR span(std::array<T, N>& arr) noexcept
+        : storage_(arr.data(), N)
+    {}
+
+    template <typename T, std::size_t N, std::size_t E = Extent,
+              typename std::enable_if<
+                  (E == dynamic_extent || N == E) &&
+                      detail::is_container_element_type_compatible<
+                          const std::array<T, N>&, ElementType>::value,
+                  int>::type = 0>
+    TCB_SPAN_ARRAY_CONSTEXPR span(const std::array<T, N>& arr) noexcept
+        : storage_(arr.data(), N)
+    {}
+
+    template <
+        typename Container, std::size_t E = Extent,
+        typename std::enable_if<
+            E == dynamic_extent && detail::is_container<Container>::value &&
+                detail::is_container_element_type_compatible<
+                    Container&, ElementType>::value,
+            int>::type = 0>
+    constexpr span(Container& cont)
+        : storage_(detail::data(cont), detail::size(cont))
+    {}
+
+    template <
+        typename Container, std::size_t E = Extent,
+        typename std::enable_if<
+            E == dynamic_extent && detail::is_container<Container>::value &&
+                detail::is_container_element_type_compatible<
+                    const Container&, ElementType>::value,
+            int>::type = 0>
+    constexpr span(const Container& cont)
+        : storage_(detail::data(cont), detail::size(cont))
+    {}
+
+    constexpr span(const span& other) noexcept = default;
+
+    template <typename OtherElementType, std::size_t OtherExtent,
+              typename std::enable_if<
+                  (Extent == dynamic_extent || OtherExtent == dynamic_extent ||
+                   Extent == OtherExtent) &&
+                      std::is_convertible<OtherElementType (*)[],
+                                          ElementType (*)[]>::value,
+                  int>::type = 0>
+    constexpr span(const span<OtherElementType, OtherExtent>& other) noexcept
+        : storage_(other.data(), other.size())
+    {}
+
+    ~span() noexcept = default;
+
+    TCB_SPAN_CONSTEXPR_ASSIGN span&
+    operator=(const span& other) noexcept = default;
+
+    // [span.sub], span subviews
+    template <std::size_t Count>
+    TCB_SPAN_CONSTEXPR11 span<element_type, Count> first() const
+    {
+        TCB_SPAN_EXPECT(Count <= size());
+        return {data(), Count};
+    }
+
+    template <std::size_t Count>
+    TCB_SPAN_CONSTEXPR11 span<element_type, Count> last() const
+    {
+        TCB_SPAN_EXPECT(Count <= size());
+        return {data() + (size() - Count), Count};
+    }
+
+    template <std::size_t Offset, std::size_t Count = dynamic_extent>
+    using subspan_return_t =
+        span<ElementType, Count != dynamic_extent
+                              ? Count
+                              : (Extent != dynamic_extent ? Extent - Offset
+                                                          : dynamic_extent)>;
+
+    template <std::size_t Offset, std::size_t Count = dynamic_extent>
+    TCB_SPAN_CONSTEXPR11 subspan_return_t<Offset, Count> subspan() const
+    {
+        TCB_SPAN_EXPECT(Offset <= size() &&
+                        (Count == dynamic_extent || Offset + Count <= size()));
+        return {data() + Offset,
+                Count != dynamic_extent ? Count : size() - Offset};
+    }
+
+    TCB_SPAN_CONSTEXPR11 span<element_type, dynamic_extent>
+    first(size_type count) const
+    {
+        TCB_SPAN_EXPECT(count <= size());
+        return {data(), count};
+    }
+
+    TCB_SPAN_CONSTEXPR11 span<element_type, dynamic_extent>
+    last(size_type count) const
+    {
+        TCB_SPAN_EXPECT(count <= size());
+        return {data() + (size() - count), count};
+    }
+
+    TCB_SPAN_CONSTEXPR11 span<element_type, dynamic_extent>
+    subspan(size_type offset, size_type count = dynamic_extent) const
+    {
+        TCB_SPAN_EXPECT(offset <= size() &&
+                        (count == dynamic_extent || offset + count <= size()));
+        return {data() + offset,
+                count == dynamic_extent ? size() - offset : count};
+    }
+
+    // [span.obs], span observers
+    constexpr size_type size() const noexcept { return storage_.size; }
+
+    constexpr size_type size_bytes() const noexcept
+    {
+        return size() * sizeof(element_type);
+    }
+
+    TCB_SPAN_NODISCARD constexpr bool empty() const noexcept
+    {
+        return size() == 0;
+    }
+
+    // [span.elem], span element access
+    TCB_SPAN_CONSTEXPR11 reference operator[](size_type idx) const
+    {
+        TCB_SPAN_EXPECT(idx < size());
+        return *(data() + idx);
+    }
+
+    TCB_SPAN_CONSTEXPR11 reference front() const
+    {
+        TCB_SPAN_EXPECT(!empty());
+        return *data();
+    }
+
+    TCB_SPAN_CONSTEXPR11 reference back() const
+    {
+        TCB_SPAN_EXPECT(!empty());
+        return *(data() + (size() - 1));
+    }
+
+    constexpr pointer data() const noexcept { return storage_.ptr; }
+
+    // [span.iterators], span iterator support
+    constexpr iterator begin() const noexcept { return data(); }
+
+    constexpr iterator end() const noexcept { return data() + size(); }
+
+    TCB_SPAN_ARRAY_CONSTEXPR reverse_iterator rbegin() const noexcept
+    {
+        return reverse_iterator(end());
+    }
+
+    TCB_SPAN_ARRAY_CONSTEXPR reverse_iterator rend() const noexcept
+    {
+        return reverse_iterator(begin());
+    }
+
+private:
+    storage_type storage_{};
+};
+
+#ifdef TCB_SPAN_HAVE_DEDUCTION_GUIDES
+
+/* Deduction Guides */
+template <class T, size_t N>
+span(T (&)[N])->span<T, N>;
+
+template <class T, size_t N>
+span(std::array<T, N>&)->span<T, N>;
+
+template <class T, size_t N>
+span(const std::array<T, N>&)->span<const T, N>;
+
+template <class Container>
+span(Container&)->span<typename std::remove_reference<
+    decltype(*detail::data(std::declval<Container&>()))>::type>;
+
+template <class Container>
+span(const Container&)->span<const typename Container::value_type>;
+
+#endif // TCB_HAVE_DEDUCTION_GUIDES
+
+template <typename ElementType, std::size_t Extent>
+constexpr span<ElementType, Extent>
+make_span(span<ElementType, Extent> s) noexcept
+{
+    return s;
+}
+
+template <typename T, std::size_t N>
+constexpr span<T, N> make_span(T (&arr)[N]) noexcept
+{
+    return {arr};
+}
+
+template <typename T, std::size_t N>
+TCB_SPAN_ARRAY_CONSTEXPR span<T, N> make_span(std::array<T, N>& arr) noexcept
+{
+    return {arr};
+}
+
+template <typename T, std::size_t N>
+TCB_SPAN_ARRAY_CONSTEXPR span<const T, N>
+make_span(const std::array<T, N>& arr) noexcept
+{
+    return {arr};
+}
+
+template <typename Container>
+constexpr span<typename std::remove_reference<
+    decltype(*detail::data(std::declval<Container&>()))>::type>
+make_span(Container& cont)
+{
+    return {cont};
+}
+
+template <typename Container>
+constexpr span<const typename Container::value_type>
+make_span(const Container& cont)
+{
+    return {cont};
+}
+
+template <typename ElementType, std::size_t Extent>
+span<const byte, ((Extent == dynamic_extent) ? dynamic_extent
+                                             : sizeof(ElementType) * Extent)>
+as_bytes(span<ElementType, Extent> s) noexcept
+{
+    return {reinterpret_cast<const byte*>(s.data()), s.size_bytes()};
+}
+
+template <
+    class ElementType, size_t Extent,
+    typename std::enable_if<!std::is_const<ElementType>::value, int>::type = 0>
+span<byte, ((Extent == dynamic_extent) ? dynamic_extent
+                                       : sizeof(ElementType) * Extent)>
+as_writable_bytes(span<ElementType, Extent> s) noexcept
+{
+    return {reinterpret_cast<byte*>(s.data()), s.size_bytes()};
+}
+
+template <std::size_t N, typename E, std::size_t S>
+constexpr auto get(span<E, S> s) -> decltype(s[N])
+{
+    return s[N];
+}
+
+} // namespace TCB_SPAN_NAMESPACE_NAME
+
+namespace std {
+
+template <typename ElementType, size_t Extent>
+class tuple_size<TCB_SPAN_NAMESPACE_NAME::span<ElementType, Extent>>
+    : public integral_constant<size_t, Extent> {};
+
+template <typename ElementType>
+class tuple_size<TCB_SPAN_NAMESPACE_NAME::span<
+    ElementType, TCB_SPAN_NAMESPACE_NAME::dynamic_extent>>; // not defined
+
+template <size_t I, typename ElementType, size_t Extent>
+class tuple_element<I, TCB_SPAN_NAMESPACE_NAME::span<ElementType, Extent>> {
+public:
+    static_assert(Extent != TCB_SPAN_NAMESPACE_NAME::dynamic_extent &&
+                      I < Extent,
+                  "");
+    using type = ElementType;
+};
+
+} // end namespace std
+
+#endif // TCB_SPAN_HPP_INCLUDED
diff --git a/src/w_wad.c b/src/w_wad.c
index 78d26f9056c16e818d0384d1f91f2237d8b8024b..abbae94a69721ef1bab5fb9405f4e52086a74e0f 100644
--- a/src/w_wad.c
+++ b/src/w_wad.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -1677,6 +1677,44 @@ lumpnum_t W_CheckNumForNameInBlock(const char *name, const char *blockstart, con
 	return LUMPERROR;
 }
 
+//
+// W_CheckNumForNameInFolder
+// Checks only in PK3s in the specified folder
+//
+lumpnum_t W_CheckNumForNameInFolder(const char *lump, const char *folder)
+{
+	INT32 i;
+	lumpnum_t fsid, feid;
+	lumpnum_t check = INT16_MAX;
+
+	// scan wad files backwards so patch lump files take precedence
+	for (i = numwadfiles - 1; i >= 0; i--)
+	{
+		if (wadfiles[i]->type == RET_PK3)
+		{
+			fsid = W_CheckNumForFolderStartPK3(folder, (UINT16)i, 0);
+			if (fsid == INT16_MAX)
+			{
+				continue; // Start doesn't exist?
+			}
+
+			feid = W_CheckNumForFolderEndPK3(folder, (UINT16)i, fsid);
+			if (feid == INT16_MAX)
+			{
+				continue; // End doesn't exist?
+			}
+
+			check = W_CheckNumForLongNamePwad(lump, (UINT16)i, fsid);
+			if (check < feid)
+			{
+				return (i<<16) + check; // found it, in our constraints
+			}
+		}
+	}
+
+	return LUMPERROR;
+}
+
 // Used by Lua. Case sensitive lump checking, quickly...
 #include "fastcmp.h"
 UINT8 W_LumpExists(const char *name)
@@ -2282,9 +2320,7 @@ void W_UnlockCachedPatch(void *patch)
 #ifdef HWRENDER
 	if (rendermode == render_opengl)
 		HWR_UnlockCachedPatch((GLPatch_t *)((patch_t *)patch)->hardware);
-	else
 #endif
-		Z_Unlock(patch);
 }
 
 void *W_CachePatchName(const char *name, INT32 tag)
diff --git a/src/w_wad.h b/src/w_wad.h
index 80e0e32fd585faaddcaf24dd8167e3f694d388f2..2358213f824d7da7856053bcb8e228da2f1b6fa1 100644
--- a/src/w_wad.h
+++ b/src/w_wad.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -18,8 +18,8 @@
 #include "hardware/hw_data.h"
 #endif
 
-#ifdef __GNUG__
-#pragma interface
+#ifdef __cplusplus
+extern "C" {
 #endif
 
 // a raw entry of the wad directory
@@ -194,6 +194,7 @@ lumpnum_t W_CheckNumForLongName(const char *name);
 lumpnum_t W_GetNumForName(const char *name); // like W_CheckNumForName but I_Error on LUMPERROR
 lumpnum_t W_GetNumForLongName(const char *name);
 lumpnum_t W_CheckNumForNameInBlock(const char *name, const char *blockstart, const char *blockend);
+lumpnum_t W_CheckNumForNameInFolder(const char *lump, const char *folder);
 UINT8 W_LumpExists(const char *name); // Lua uses this.
 
 size_t W_LumpLengthPwad(UINT16 wad, UINT16 lump);
@@ -236,4 +237,8 @@ void W_VerifyFileMD5(UINT16 wadfilenum, const char *matchmd5);
 
 int W_VerifyNMUSlumps(const char *filename, boolean exit_on_error);
 
+#ifdef __cplusplus
+} // extern "C"
+#endif
+
 #endif // __W_WAD__
diff --git a/src/z_zone.h b/src/z_zone.h
index ce7af4a159555e3a6c2be83e8d0eadcf8a7a69eb..4e333b4e1b88cbeb18b266e8d23ddc44c35d73ad 100644
--- a/src/z_zone.h
+++ b/src/z_zone.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -18,6 +18,10 @@
 #include "doomdef.h"
 #include "doomtype.h"
 
+#ifdef __cplusplus
+extern "C" {
+#endif
+
 #ifdef __GNUC__ // __attribute__ ((X))
 #if (__GNUC__ > 4) || (__GNUC__ == 4 && (__GNUC_MINOR__ >= 3 || (__GNUC_MINOR__ == 2 && __GNUC_PATCHLEVEL__ >= 5)))
 #define FUNCALLOC(X) __attribute__((alloc_size(X)))
@@ -152,6 +156,9 @@ size_t Z_TagsUsage(INT32 lowtag, INT32 hightag);
 // Miscellaneous functions
 //
 char *Z_StrDup(const char *in);
-#define Z_Unlock(p) (void)p // TODO: remove this now that NDS code has been removed
+
+#ifdef __cplusplus
+} // extern "C"
+#endif
 
 #endif