Rename triggers to mapobjects
authorMario <mario@smbclan.net>
Sat, 9 Jun 2018 15:52:03 +0000 (01:52 +1000)
committerMario <mario@smbclan.net>
Sat, 9 Jun 2018 15:52:03 +0000 (01:52 +1000)
297 files changed:
qcsrc/client/main.qc
qcsrc/client/view.qc
qcsrc/common/_all.inc
qcsrc/common/mapobjects/_mod.inc [new file with mode: 0644]
qcsrc/common/mapobjects/_mod.qh [new file with mode: 0644]
qcsrc/common/mapobjects/defs.qh [new file with mode: 0644]
qcsrc/common/mapobjects/func/_mod.inc [new file with mode: 0644]
qcsrc/common/mapobjects/func/_mod.qh [new file with mode: 0644]
qcsrc/common/mapobjects/func/bobbing.qc [new file with mode: 0644]
qcsrc/common/mapobjects/func/bobbing.qh [new file with mode: 0644]
qcsrc/common/mapobjects/func/breakable.qc [new file with mode: 0644]
qcsrc/common/mapobjects/func/breakable.qh [new file with mode: 0644]
qcsrc/common/mapobjects/func/button.qc [new file with mode: 0644]
qcsrc/common/mapobjects/func/button.qh [new file with mode: 0644]
qcsrc/common/mapobjects/func/conveyor.qc [new file with mode: 0644]
qcsrc/common/mapobjects/func/conveyor.qh [new file with mode: 0644]
qcsrc/common/mapobjects/func/door.qc [new file with mode: 0644]
qcsrc/common/mapobjects/func/door.qh [new file with mode: 0644]
qcsrc/common/mapobjects/func/door_rotating.qc [new file with mode: 0644]
qcsrc/common/mapobjects/func/door_rotating.qh [new file with mode: 0644]
qcsrc/common/mapobjects/func/door_secret.qc [new file with mode: 0644]
qcsrc/common/mapobjects/func/door_secret.qh [new file with mode: 0644]
qcsrc/common/mapobjects/func/fourier.qc [new file with mode: 0644]
qcsrc/common/mapobjects/func/fourier.qh [new file with mode: 0644]
qcsrc/common/mapobjects/func/include.qc [new file with mode: 0644]
qcsrc/common/mapobjects/func/include.qh [new file with mode: 0644]
qcsrc/common/mapobjects/func/ladder.qc [new file with mode: 0644]
qcsrc/common/mapobjects/func/ladder.qh [new file with mode: 0644]
qcsrc/common/mapobjects/func/pendulum.qc [new file with mode: 0644]
qcsrc/common/mapobjects/func/pendulum.qh [new file with mode: 0644]
qcsrc/common/mapobjects/func/plat.qc [new file with mode: 0644]
qcsrc/common/mapobjects/func/plat.qh [new file with mode: 0644]
qcsrc/common/mapobjects/func/pointparticles.qc [new file with mode: 0644]
qcsrc/common/mapobjects/func/pointparticles.qh [new file with mode: 0644]
qcsrc/common/mapobjects/func/rainsnow.qc [new file with mode: 0644]
qcsrc/common/mapobjects/func/rainsnow.qh [new file with mode: 0644]
qcsrc/common/mapobjects/func/rotating.qc [new file with mode: 0644]
qcsrc/common/mapobjects/func/rotating.qh [new file with mode: 0644]
qcsrc/common/mapobjects/func/stardust.qc [new file with mode: 0644]
qcsrc/common/mapobjects/func/stardust.qh [new file with mode: 0644]
qcsrc/common/mapobjects/func/train.qc [new file with mode: 0644]
qcsrc/common/mapobjects/func/train.qh [new file with mode: 0644]
qcsrc/common/mapobjects/func/vectormamamam.qc [new file with mode: 0644]
qcsrc/common/mapobjects/func/vectormamamam.qh [new file with mode: 0644]
qcsrc/common/mapobjects/include.qc [new file with mode: 0644]
qcsrc/common/mapobjects/include.qh [new file with mode: 0644]
qcsrc/common/mapobjects/misc/_mod.inc [new file with mode: 0644]
qcsrc/common/mapobjects/misc/_mod.qh [new file with mode: 0644]
qcsrc/common/mapobjects/misc/corner.qc [new file with mode: 0644]
qcsrc/common/mapobjects/misc/corner.qh [new file with mode: 0644]
qcsrc/common/mapobjects/misc/follow.qc [new file with mode: 0644]
qcsrc/common/mapobjects/misc/follow.qh [new file with mode: 0644]
qcsrc/common/mapobjects/misc/include.qc [new file with mode: 0644]
qcsrc/common/mapobjects/misc/include.qh [new file with mode: 0644]
qcsrc/common/mapobjects/misc/laser.qc [new file with mode: 0644]
qcsrc/common/mapobjects/misc/laser.qh [new file with mode: 0644]
qcsrc/common/mapobjects/misc/teleport_dest.qc [new file with mode: 0644]
qcsrc/common/mapobjects/misc/teleport_dest.qh [new file with mode: 0644]
qcsrc/common/mapobjects/platforms.qc [new file with mode: 0644]
qcsrc/common/mapobjects/platforms.qh [new file with mode: 0644]
qcsrc/common/mapobjects/subs.qc [new file with mode: 0644]
qcsrc/common/mapobjects/subs.qh [new file with mode: 0644]
qcsrc/common/mapobjects/target/_mod.inc [new file with mode: 0644]
qcsrc/common/mapobjects/target/_mod.qh [new file with mode: 0644]
qcsrc/common/mapobjects/target/changelevel.qc [new file with mode: 0644]
qcsrc/common/mapobjects/target/changelevel.qh [new file with mode: 0644]
qcsrc/common/mapobjects/target/include.qc [new file with mode: 0644]
qcsrc/common/mapobjects/target/include.qh [new file with mode: 0644]
qcsrc/common/mapobjects/target/kill.qc [new file with mode: 0644]
qcsrc/common/mapobjects/target/kill.qh [new file with mode: 0644]
qcsrc/common/mapobjects/target/levelwarp.qc [new file with mode: 0644]
qcsrc/common/mapobjects/target/levelwarp.qh [new file with mode: 0644]
qcsrc/common/mapobjects/target/location.qc [new file with mode: 0644]
qcsrc/common/mapobjects/target/location.qh [new file with mode: 0644]
qcsrc/common/mapobjects/target/music.qc [new file with mode: 0644]
qcsrc/common/mapobjects/target/music.qh [new file with mode: 0644]
qcsrc/common/mapobjects/target/spawn.qc [new file with mode: 0644]
qcsrc/common/mapobjects/target/spawn.qh [new file with mode: 0644]
qcsrc/common/mapobjects/target/spawnpoint.qc [new file with mode: 0644]
qcsrc/common/mapobjects/target/spawnpoint.qh [new file with mode: 0644]
qcsrc/common/mapobjects/target/speaker.qc [new file with mode: 0644]
qcsrc/common/mapobjects/target/speaker.qh [new file with mode: 0644]
qcsrc/common/mapobjects/target/voicescript.qc [new file with mode: 0644]
qcsrc/common/mapobjects/target/voicescript.qh [new file with mode: 0644]
qcsrc/common/mapobjects/teleporters.qc [new file with mode: 0644]
qcsrc/common/mapobjects/teleporters.qh [new file with mode: 0644]
qcsrc/common/mapobjects/trigger/_mod.inc [new file with mode: 0644]
qcsrc/common/mapobjects/trigger/_mod.qh [new file with mode: 0644]
qcsrc/common/mapobjects/trigger/counter.qc [new file with mode: 0644]
qcsrc/common/mapobjects/trigger/counter.qh [new file with mode: 0644]
qcsrc/common/mapobjects/trigger/delay.qc [new file with mode: 0644]
qcsrc/common/mapobjects/trigger/delay.qh [new file with mode: 0644]
qcsrc/common/mapobjects/trigger/disablerelay.qc [new file with mode: 0644]
qcsrc/common/mapobjects/trigger/disablerelay.qh [new file with mode: 0644]
qcsrc/common/mapobjects/trigger/flipflop.qc [new file with mode: 0644]
qcsrc/common/mapobjects/trigger/flipflop.qh [new file with mode: 0644]
qcsrc/common/mapobjects/trigger/gamestart.qc [new file with mode: 0644]
qcsrc/common/mapobjects/trigger/gamestart.qh [new file with mode: 0644]
qcsrc/common/mapobjects/trigger/gravity.qc [new file with mode: 0644]
qcsrc/common/mapobjects/trigger/gravity.qh [new file with mode: 0644]
qcsrc/common/mapobjects/trigger/heal.qc [new file with mode: 0644]
qcsrc/common/mapobjects/trigger/heal.qh [new file with mode: 0644]
qcsrc/common/mapobjects/trigger/hurt.qc [new file with mode: 0644]
qcsrc/common/mapobjects/trigger/hurt.qh [new file with mode: 0644]
qcsrc/common/mapobjects/trigger/impulse.qc [new file with mode: 0644]
qcsrc/common/mapobjects/trigger/impulse.qh [new file with mode: 0644]
qcsrc/common/mapobjects/trigger/include.qc [new file with mode: 0644]
qcsrc/common/mapobjects/trigger/include.qh [new file with mode: 0644]
qcsrc/common/mapobjects/trigger/jumppads.qc [new file with mode: 0644]
qcsrc/common/mapobjects/trigger/jumppads.qh [new file with mode: 0644]
qcsrc/common/mapobjects/trigger/keylock.qc [new file with mode: 0644]
qcsrc/common/mapobjects/trigger/keylock.qh [new file with mode: 0644]
qcsrc/common/mapobjects/trigger/magicear.qc [new file with mode: 0644]
qcsrc/common/mapobjects/trigger/magicear.qh [new file with mode: 0644]
qcsrc/common/mapobjects/trigger/monoflop.qc [new file with mode: 0644]
qcsrc/common/mapobjects/trigger/monoflop.qh [new file with mode: 0644]
qcsrc/common/mapobjects/trigger/multi.qc [new file with mode: 0644]
qcsrc/common/mapobjects/trigger/multi.qh [new file with mode: 0644]
qcsrc/common/mapobjects/trigger/multivibrator.qc [new file with mode: 0644]
qcsrc/common/mapobjects/trigger/multivibrator.qh [new file with mode: 0644]
qcsrc/common/mapobjects/trigger/relay.qc [new file with mode: 0644]
qcsrc/common/mapobjects/trigger/relay.qh [new file with mode: 0644]
qcsrc/common/mapobjects/trigger/relay_activators.qc [new file with mode: 0644]
qcsrc/common/mapobjects/trigger/relay_activators.qh [new file with mode: 0644]
qcsrc/common/mapobjects/trigger/relay_if.qc [new file with mode: 0644]
qcsrc/common/mapobjects/trigger/relay_if.qh [new file with mode: 0644]
qcsrc/common/mapobjects/trigger/relay_teamcheck.qc [new file with mode: 0644]
qcsrc/common/mapobjects/trigger/relay_teamcheck.qh [new file with mode: 0644]
qcsrc/common/mapobjects/trigger/secret.qc [new file with mode: 0644]
qcsrc/common/mapobjects/trigger/secret.qh [new file with mode: 0644]
qcsrc/common/mapobjects/trigger/swamp.qc [new file with mode: 0644]
qcsrc/common/mapobjects/trigger/swamp.qh [new file with mode: 0644]
qcsrc/common/mapobjects/trigger/teleport.qc [new file with mode: 0644]
qcsrc/common/mapobjects/trigger/teleport.qh [new file with mode: 0644]
qcsrc/common/mapobjects/trigger/viewloc.qc [new file with mode: 0644]
qcsrc/common/mapobjects/trigger/viewloc.qh [new file with mode: 0644]
qcsrc/common/mapobjects/triggers.qc [new file with mode: 0644]
qcsrc/common/mapobjects/triggers.qh [new file with mode: 0644]
qcsrc/common/monsters/sv_monsters.qc
qcsrc/common/mutators/mutator/buffs/sv_buffs.qc
qcsrc/common/physics/player.qc
qcsrc/common/t_items.qc
qcsrc/common/triggers/_mod.inc [deleted file]
qcsrc/common/triggers/_mod.qh [deleted file]
qcsrc/common/triggers/defs.qh [deleted file]
qcsrc/common/triggers/func/_mod.inc [deleted file]
qcsrc/common/triggers/func/_mod.qh [deleted file]
qcsrc/common/triggers/func/bobbing.qc [deleted file]
qcsrc/common/triggers/func/bobbing.qh [deleted file]
qcsrc/common/triggers/func/breakable.qc [deleted file]
qcsrc/common/triggers/func/breakable.qh [deleted file]
qcsrc/common/triggers/func/button.qc [deleted file]
qcsrc/common/triggers/func/button.qh [deleted file]
qcsrc/common/triggers/func/conveyor.qc [deleted file]
qcsrc/common/triggers/func/conveyor.qh [deleted file]
qcsrc/common/triggers/func/door.qc [deleted file]
qcsrc/common/triggers/func/door.qh [deleted file]
qcsrc/common/triggers/func/door_rotating.qc [deleted file]
qcsrc/common/triggers/func/door_rotating.qh [deleted file]
qcsrc/common/triggers/func/door_secret.qc [deleted file]
qcsrc/common/triggers/func/door_secret.qh [deleted file]
qcsrc/common/triggers/func/fourier.qc [deleted file]
qcsrc/common/triggers/func/fourier.qh [deleted file]
qcsrc/common/triggers/func/include.qc [deleted file]
qcsrc/common/triggers/func/include.qh [deleted file]
qcsrc/common/triggers/func/ladder.qc [deleted file]
qcsrc/common/triggers/func/ladder.qh [deleted file]
qcsrc/common/triggers/func/pendulum.qc [deleted file]
qcsrc/common/triggers/func/pendulum.qh [deleted file]
qcsrc/common/triggers/func/plat.qc [deleted file]
qcsrc/common/triggers/func/plat.qh [deleted file]
qcsrc/common/triggers/func/pointparticles.qc [deleted file]
qcsrc/common/triggers/func/pointparticles.qh [deleted file]
qcsrc/common/triggers/func/rainsnow.qc [deleted file]
qcsrc/common/triggers/func/rainsnow.qh [deleted file]
qcsrc/common/triggers/func/rotating.qc [deleted file]
qcsrc/common/triggers/func/rotating.qh [deleted file]
qcsrc/common/triggers/func/stardust.qc [deleted file]
qcsrc/common/triggers/func/stardust.qh [deleted file]
qcsrc/common/triggers/func/train.qc [deleted file]
qcsrc/common/triggers/func/train.qh [deleted file]
qcsrc/common/triggers/func/vectormamamam.qc [deleted file]
qcsrc/common/triggers/func/vectormamamam.qh [deleted file]
qcsrc/common/triggers/include.qc [deleted file]
qcsrc/common/triggers/include.qh [deleted file]
qcsrc/common/triggers/misc/_mod.inc [deleted file]
qcsrc/common/triggers/misc/_mod.qh [deleted file]
qcsrc/common/triggers/misc/corner.qc [deleted file]
qcsrc/common/triggers/misc/corner.qh [deleted file]
qcsrc/common/triggers/misc/follow.qc [deleted file]
qcsrc/common/triggers/misc/follow.qh [deleted file]
qcsrc/common/triggers/misc/include.qc [deleted file]
qcsrc/common/triggers/misc/include.qh [deleted file]
qcsrc/common/triggers/misc/laser.qc [deleted file]
qcsrc/common/triggers/misc/laser.qh [deleted file]
qcsrc/common/triggers/misc/teleport_dest.qc [deleted file]
qcsrc/common/triggers/misc/teleport_dest.qh [deleted file]
qcsrc/common/triggers/platforms.qc [deleted file]
qcsrc/common/triggers/platforms.qh [deleted file]
qcsrc/common/triggers/subs.qc [deleted file]
qcsrc/common/triggers/subs.qh [deleted file]
qcsrc/common/triggers/target/_mod.inc [deleted file]
qcsrc/common/triggers/target/_mod.qh [deleted file]
qcsrc/common/triggers/target/changelevel.qc [deleted file]
qcsrc/common/triggers/target/changelevel.qh [deleted file]
qcsrc/common/triggers/target/include.qc [deleted file]
qcsrc/common/triggers/target/include.qh [deleted file]
qcsrc/common/triggers/target/kill.qc [deleted file]
qcsrc/common/triggers/target/kill.qh [deleted file]
qcsrc/common/triggers/target/levelwarp.qc [deleted file]
qcsrc/common/triggers/target/levelwarp.qh [deleted file]
qcsrc/common/triggers/target/location.qc [deleted file]
qcsrc/common/triggers/target/location.qh [deleted file]
qcsrc/common/triggers/target/music.qc [deleted file]
qcsrc/common/triggers/target/music.qh [deleted file]
qcsrc/common/triggers/target/spawn.qc [deleted file]
qcsrc/common/triggers/target/spawn.qh [deleted file]
qcsrc/common/triggers/target/spawnpoint.qc [deleted file]
qcsrc/common/triggers/target/spawnpoint.qh [deleted file]
qcsrc/common/triggers/target/speaker.qc [deleted file]
qcsrc/common/triggers/target/speaker.qh [deleted file]
qcsrc/common/triggers/target/voicescript.qc [deleted file]
qcsrc/common/triggers/target/voicescript.qh [deleted file]
qcsrc/common/triggers/teleporters.qc [deleted file]
qcsrc/common/triggers/teleporters.qh [deleted file]
qcsrc/common/triggers/trigger/_mod.inc [deleted file]
qcsrc/common/triggers/trigger/_mod.qh [deleted file]
qcsrc/common/triggers/trigger/counter.qc [deleted file]
qcsrc/common/triggers/trigger/counter.qh [deleted file]
qcsrc/common/triggers/trigger/delay.qc [deleted file]
qcsrc/common/triggers/trigger/delay.qh [deleted file]
qcsrc/common/triggers/trigger/disablerelay.qc [deleted file]
qcsrc/common/triggers/trigger/disablerelay.qh [deleted file]
qcsrc/common/triggers/trigger/flipflop.qc [deleted file]
qcsrc/common/triggers/trigger/flipflop.qh [deleted file]
qcsrc/common/triggers/trigger/gamestart.qc [deleted file]
qcsrc/common/triggers/trigger/gamestart.qh [deleted file]
qcsrc/common/triggers/trigger/gravity.qc [deleted file]
qcsrc/common/triggers/trigger/gravity.qh [deleted file]
qcsrc/common/triggers/trigger/heal.qc [deleted file]
qcsrc/common/triggers/trigger/heal.qh [deleted file]
qcsrc/common/triggers/trigger/hurt.qc [deleted file]
qcsrc/common/triggers/trigger/hurt.qh [deleted file]
qcsrc/common/triggers/trigger/impulse.qc [deleted file]
qcsrc/common/triggers/trigger/impulse.qh [deleted file]
qcsrc/common/triggers/trigger/include.qc [deleted file]
qcsrc/common/triggers/trigger/include.qh [deleted file]
qcsrc/common/triggers/trigger/jumppads.qc [deleted file]
qcsrc/common/triggers/trigger/jumppads.qh [deleted file]
qcsrc/common/triggers/trigger/keylock.qc [deleted file]
qcsrc/common/triggers/trigger/keylock.qh [deleted file]
qcsrc/common/triggers/trigger/magicear.qc [deleted file]
qcsrc/common/triggers/trigger/magicear.qh [deleted file]
qcsrc/common/triggers/trigger/monoflop.qc [deleted file]
qcsrc/common/triggers/trigger/monoflop.qh [deleted file]
qcsrc/common/triggers/trigger/multi.qc [deleted file]
qcsrc/common/triggers/trigger/multi.qh [deleted file]
qcsrc/common/triggers/trigger/multivibrator.qc [deleted file]
qcsrc/common/triggers/trigger/multivibrator.qh [deleted file]
qcsrc/common/triggers/trigger/relay.qc [deleted file]
qcsrc/common/triggers/trigger/relay.qh [deleted file]
qcsrc/common/triggers/trigger/relay_activators.qc [deleted file]
qcsrc/common/triggers/trigger/relay_activators.qh [deleted file]
qcsrc/common/triggers/trigger/relay_if.qc [deleted file]
qcsrc/common/triggers/trigger/relay_if.qh [deleted file]
qcsrc/common/triggers/trigger/relay_teamcheck.qc [deleted file]
qcsrc/common/triggers/trigger/relay_teamcheck.qh [deleted file]
qcsrc/common/triggers/trigger/secret.qc [deleted file]
qcsrc/common/triggers/trigger/secret.qh [deleted file]
qcsrc/common/triggers/trigger/swamp.qc [deleted file]
qcsrc/common/triggers/trigger/swamp.qh [deleted file]
qcsrc/common/triggers/trigger/teleport.qc [deleted file]
qcsrc/common/triggers/trigger/teleport.qh [deleted file]
qcsrc/common/triggers/trigger/viewloc.qc [deleted file]
qcsrc/common/triggers/trigger/viewloc.qh [deleted file]
qcsrc/common/triggers/triggers.qc [deleted file]
qcsrc/common/triggers/triggers.qh [deleted file]
qcsrc/common/vehicles/vehicle/racer.qc
qcsrc/common/weapons/weapon/porto.qc
qcsrc/lib/warpzone/server.qc
qcsrc/server/bot/default/havocbot/havocbot.qc
qcsrc/server/bot/default/navigation.qc
qcsrc/server/cheats.qc
qcsrc/server/client.qc
qcsrc/server/command/cmd.qc
qcsrc/server/g_models.qc
qcsrc/server/g_subs.qc
qcsrc/server/g_world.qc
qcsrc/server/item_key.qc
qcsrc/server/miscfunctions.qc
qcsrc/server/mutators/gamemode.qh
qcsrc/server/mutators/mutator.qh
qcsrc/server/player.qc
qcsrc/server/portals.qc
qcsrc/server/race.qc
qcsrc/server/spawnpoints.qc
qcsrc/server/weapons/throwing.qc

index 9222d97..8e0c76d 100644 (file)
@@ -24,7 +24,7 @@
 #include <common/net_linked.qh>
 #include <common/net_notice.qh>
 #include <common/scores.qh>
-#include <common/triggers/include.qh>
+#include <common/mapobjects/include.qh>
 #include <common/vehicles/all.qh>
 #include <lib/csqcmodel/cl_model.qh>
 #include <lib/csqcmodel/interpolate.qh>
index f2196cd..fa5a792 100644 (file)
@@ -22,7 +22,7 @@
 #include <common/gamemodes/_mod.qh>
 #include <common/physics/player.qh>
 #include <common/stats.qh>
-#include <common/triggers/target/music.qh>
+#include <common/mapobjects/target/music.qh>
 #include <common/teams.qh>
 #include <common/wepent.qh>
 
@@ -32,7 +32,7 @@
 #include <common/weapons/_all.qh>
 #include <common/mutators/mutator/overkill/oknex.qh>
 #include <common/viewloc.qh>
-#include <common/triggers/trigger/viewloc.qh>
+#include <common/mapobjects/trigger/viewloc.qh>
 #include <common/minigames/cl_minigames.qh>
 #include <common/minigames/cl_minigames_hud.qh>
 
index 32dbf52..708b4e9 100644 (file)
@@ -22,7 +22,7 @@ noref float autocvar_net_connecttimeout = 30;
 
 #ifdef GAMEQC
 #include "physics/all.inc"
-#include "triggers/include.qc"
+#include "mapobjects/include.qc"
 #include "viewloc.qc"
 #endif
 
diff --git a/qcsrc/common/mapobjects/_mod.inc b/qcsrc/common/mapobjects/_mod.inc
new file mode 100644 (file)
index 0000000..efc8384
--- /dev/null
@@ -0,0 +1,11 @@
+// generated file; do not modify
+#include <common/mapobjects/include.qc>
+#include <common/mapobjects/platforms.qc>
+#include <common/mapobjects/subs.qc>
+#include <common/mapobjects/teleporters.qc>
+#include <common/mapobjects/triggers.qc>
+
+#include <common/mapobjects/func/_mod.inc>
+#include <common/mapobjects/misc/_mod.inc>
+#include <common/mapobjects/target/_mod.inc>
+#include <common/mapobjects/trigger/_mod.inc>
diff --git a/qcsrc/common/mapobjects/_mod.qh b/qcsrc/common/mapobjects/_mod.qh
new file mode 100644 (file)
index 0000000..11854c0
--- /dev/null
@@ -0,0 +1,11 @@
+// generated file; do not modify
+#include <common/mapobjects/include.qh>
+#include <common/mapobjects/platforms.qh>
+#include <common/mapobjects/subs.qh>
+#include <common/mapobjects/teleporters.qh>
+#include <common/mapobjects/triggers.qh>
+
+#include <common/mapobjects/func/_mod.qh>
+#include <common/mapobjects/misc/_mod.qh>
+#include <common/mapobjects/target/_mod.qh>
+#include <common/mapobjects/trigger/_mod.qh>
diff --git a/qcsrc/common/mapobjects/defs.qh b/qcsrc/common/mapobjects/defs.qh
new file mode 100644 (file)
index 0000000..45afb51
--- /dev/null
@@ -0,0 +1,41 @@
+#pragma once
+
+//-----------
+// SPAWNFLAGS
+//-----------
+const int START_ENABLED = BIT(0);
+const int START_DISABLED = BIT(0);
+const int ALL_ENTITIES = BIT(1);
+const int ON_MAPLOAD = BIT(1);
+const int INVERT_TEAMS = BIT(2);
+const int CRUSH = BIT(2);
+const int NOSPLASH = BIT(8); // generic anti-splashdamage spawnflag
+const int ONLY_PLAYERS = BIT(14);
+
+// triggers
+const int SPAWNFLAG_NOMESSAGE = BIT(0);
+const int SPAWNFLAG_NOTOUCH = BIT(0);
+
+//----------
+// SENDFLAGS
+//----------
+const int SF_TRIGGER_INIT = BIT(0);
+const int SF_TRIGGER_UPDATE = BIT(1);
+const int SF_TRIGGER_RESET = BIT(2);
+
+//----------------
+// STATES & ACTIVE
+//----------------
+#ifdef CSQC
+// this stuff is defined in the server side engine VM, so we must define it separately here
+const int STATE_TOP = 0;
+const int STATE_BOTTOM = 1;
+const int STATE_UP = 2;
+const int STATE_DOWN = 3;
+
+const int ACTIVE_NOT = 0;
+const int ACTIVE_ACTIVE = 1;
+const int ACTIVE_IDLE = 2;
+const int ACTIVE_BUSY = 2;
+const int ACTIVE_TOGGLE = 3;
+#endif
diff --git a/qcsrc/common/mapobjects/func/_mod.inc b/qcsrc/common/mapobjects/func/_mod.inc
new file mode 100644 (file)
index 0000000..e3425e3
--- /dev/null
@@ -0,0 +1,19 @@
+// generated file; do not modify
+#include <common/mapobjects/func/bobbing.qc>
+#include <common/mapobjects/func/breakable.qc>
+#include <common/mapobjects/func/button.qc>
+#include <common/mapobjects/func/conveyor.qc>
+#include <common/mapobjects/func/door.qc>
+#include <common/mapobjects/func/door_rotating.qc>
+#include <common/mapobjects/func/door_secret.qc>
+#include <common/mapobjects/func/fourier.qc>
+#include <common/mapobjects/func/include.qc>
+#include <common/mapobjects/func/ladder.qc>
+#include <common/mapobjects/func/pendulum.qc>
+#include <common/mapobjects/func/plat.qc>
+#include <common/mapobjects/func/pointparticles.qc>
+#include <common/mapobjects/func/rainsnow.qc>
+#include <common/mapobjects/func/rotating.qc>
+#include <common/mapobjects/func/stardust.qc>
+#include <common/mapobjects/func/train.qc>
+#include <common/mapobjects/func/vectormamamam.qc>
diff --git a/qcsrc/common/mapobjects/func/_mod.qh b/qcsrc/common/mapobjects/func/_mod.qh
new file mode 100644 (file)
index 0000000..c0381a3
--- /dev/null
@@ -0,0 +1,19 @@
+// generated file; do not modify
+#include <common/mapobjects/func/bobbing.qh>
+#include <common/mapobjects/func/breakable.qh>
+#include <common/mapobjects/func/button.qh>
+#include <common/mapobjects/func/conveyor.qh>
+#include <common/mapobjects/func/door.qh>
+#include <common/mapobjects/func/door_rotating.qh>
+#include <common/mapobjects/func/door_secret.qh>
+#include <common/mapobjects/func/fourier.qh>
+#include <common/mapobjects/func/include.qh>
+#include <common/mapobjects/func/ladder.qh>
+#include <common/mapobjects/func/pendulum.qh>
+#include <common/mapobjects/func/plat.qh>
+#include <common/mapobjects/func/pointparticles.qh>
+#include <common/mapobjects/func/rainsnow.qh>
+#include <common/mapobjects/func/rotating.qh>
+#include <common/mapobjects/func/stardust.qh>
+#include <common/mapobjects/func/train.qh>
+#include <common/mapobjects/func/vectormamamam.qh>
diff --git a/qcsrc/common/mapobjects/func/bobbing.qc b/qcsrc/common/mapobjects/func/bobbing.qc
new file mode 100644 (file)
index 0000000..b647e15
--- /dev/null
@@ -0,0 +1,85 @@
+#include "bobbing.qh"
+#ifdef SVQC
+.float height;
+void func_bobbing_controller_think(entity this)
+{
+       vector v;
+       this.nextthink = time + 0.1;
+
+       if(this.owner.active != ACTIVE_ACTIVE)
+       {
+               this.owner.velocity = '0 0 0';
+               return;
+       }
+
+       // calculate sinewave using makevectors
+       makevectors((this.nextthink * this.owner.cnt + this.owner.phase * 360) * '0 1 0');
+       v = this.owner.destvec + this.owner.movedir * v_forward_y;
+       if(this.owner.classname == "func_bobbing") // don't brake stuff if the func_bobbing was killtarget'ed
+               // * 10 so it will arrive in 0.1 sec
+               this.owner.velocity = (v - this.owner.origin) * 10;
+}
+
+/*QUAKED spawnfunc_func_bobbing (0 .5 .8) ? X_AXIS Y_AXIS
+Brush model that moves back and forth on one axis (default Z).
+speed : how long one cycle takes in seconds (default 4)
+height : how far the cycle moves (default 32)
+phase : cycle timing adjustment (0-1 as a fraction of the cycle, default 0)
+noise : path/name of looping .wav file to play.
+dmg : Do this mutch dmg every .dmgtime intervall when blocked
+dmgtime : See above.
+*/
+spawnfunc(func_bobbing)
+{
+       entity controller;
+       if (this.noise != "")
+       {
+               precache_sound(this.noise);
+               soundto(MSG_INIT, this, CH_TRIGGER_SINGLE, this.noise, VOL_BASE, ATTEN_IDLE);
+       }
+       if (!this.speed)
+               this.speed = 4;
+       if (!this.height)
+               this.height = 32;
+       // center of bobbing motion
+       this.destvec = this.origin;
+       // time scale to get degrees
+       this.cnt = 360 / this.speed;
+
+       this.active = ACTIVE_ACTIVE;
+
+       // damage when blocked
+       setblocked(this, generic_plat_blocked);
+       if(this.dmg && (this.message == ""))
+               this.message = " was squished";
+    if(this.dmg && (this.message2 == ""))
+               this.message2 = "was squished by";
+       if(this.dmg && (!this.dmgtime))
+               this.dmgtime = 0.25;
+       this.dmgtime2 = time;
+
+       // how far to bob
+       if (this.spawnflags & BOBBING_XAXIS)
+               this.movedir = '1 0 0' * this.height;
+       else if (this.spawnflags & BOBBING_YAXIS)
+               this.movedir = '0 1 0' * this.height;
+       else // Z
+               this.movedir = '0 0 1' * this.height;
+
+       if (!InitMovingBrushTrigger(this))
+               return;
+
+       // wait for targets to spawn
+       controller = new(func_bobbing_controller);
+       controller.owner = this;
+       controller.nextthink = time + 1;
+       setthink(controller, func_bobbing_controller_think);
+       this.nextthink = this.ltime + 999999999;
+       setthink(this, SUB_NullThink);
+
+       // Savage: Reduce bandwith, critical on e.g. nexdm02
+       this.effects |= EF_LOWPRECISION;
+
+       // TODO make a reset function for this one
+}
+#endif
diff --git a/qcsrc/common/mapobjects/func/bobbing.qh b/qcsrc/common/mapobjects/func/bobbing.qh
new file mode 100644 (file)
index 0000000..58f7bb7
--- /dev/null
@@ -0,0 +1,5 @@
+#pragma once
+
+
+const int BOBBING_XAXIS = BIT(0);
+const int BOBBING_YAXIS = BIT(1);
diff --git a/qcsrc/common/mapobjects/func/breakable.qc b/qcsrc/common/mapobjects/func/breakable.qc
new file mode 100644 (file)
index 0000000..d09ccd5
--- /dev/null
@@ -0,0 +1,374 @@
+#include "breakable.qh"
+#ifdef SVQC
+
+#include <server/g_subs.qh>
+#include <server/g_damage.qh>
+#include <server/bot/api.qh>
+#include <common/csqcmodel_settings.qh>
+#include <lib/csqcmodel/sv_model.qh>
+#include <server/weapons/common.qh>
+
+.entity sprite;
+
+.float dmg;
+.float dmg_edge;
+.float dmg_radius;
+.float dmg_force;
+.float debrismovetype;
+.float debrissolid;
+.vector debrisvelocity;
+.vector debrisvelocityjitter;
+.vector debrisavelocityjitter;
+.float debristime;
+.float debristimejitter;
+.float debrisfadetime;
+.float debrisdamageforcescale;
+.float debrisskin;
+
+.string mdl_dead; // or "" to hide when broken
+.string debris; // space separated list of debris models
+// other fields:
+//   mdl = particle effect name
+//   count = particle effect multiplier
+//   targetname = target to trigger to unbreak the model
+//   target = targets to trigger when broken
+//   health = amount of damage it can take
+//   spawnflags:
+//     START_DISABLED: needs to be triggered to activate
+//     BREAKABLE_INDICATE_DAMAGE: indicate damage
+//     BREAKABLE_NODAMAGE: don't take direct damage (needs to be triggered to 'explode', then triggered again to restore)
+//     NOSPLASH: don't take splash damage
+// notes:
+//   for mdl_dead to work, origin must be set (using a common/origin brush).
+//   Otherwise mdl_dead will be displayed at the map origin, and nobody would
+//   want that!
+
+void func_breakable_damage(entity this, entity inflictor, entity attacker, float damage, int deathtype, .entity weaponentity, vector hitloc, vector force);
+
+//
+// func_breakable
+// - basically func_assault_destructible for general gameplay use
+//
+void LaunchDebris (entity this, string debrisname, vector force)
+{
+       entity dbr = spawn();
+       vector org = this.absmin
+                  + '1 0 0' * random() * (this.absmax.x - this.absmin.x)
+                  + '0 1 0' * random() * (this.absmax.y - this.absmin.y)
+                  + '0 0 1' * random() * (this.absmax.z - this.absmin.z);
+       setorigin(dbr, org);
+       _setmodel (dbr, debrisname );
+       dbr.skin = this.debrisskin;
+       dbr.colormap = this.colormap; // inherit team colors
+       dbr.owner = this; // do not be affected by our own explosion
+       set_movetype(dbr, this.debrismovetype);
+       dbr.solid = this.debrissolid;
+       if(dbr.solid != SOLID_BSP) // SOLID_BSP has exact collision, MAYBE this works? TODO check this out
+               setsize(dbr, '0 0 0', '0 0 0'); // needed for performance, until engine can deal better with it
+       dbr.velocity_x = this.debrisvelocity.x + this.debrisvelocityjitter.x * crandom();
+       dbr.velocity_y = this.debrisvelocity.y + this.debrisvelocityjitter.y * crandom();
+       dbr.velocity_z = this.debrisvelocity.z + this.debrisvelocityjitter.z * crandom();
+       dbr.velocity = dbr.velocity + force * this.debrisdamageforcescale;
+       dbr.angles = this.angles;
+       dbr.avelocity_x = random()*this.debrisavelocityjitter.x;
+       dbr.avelocity_y = random()*this.debrisavelocityjitter.y;
+       dbr.avelocity_z = random()*this.debrisavelocityjitter.z;
+       dbr.damageforcescale = this.debrisdamageforcescale;
+       if(dbr.damageforcescale)
+               dbr.takedamage = DAMAGE_YES;
+       SUB_SetFade(dbr, time + this.debristime + crandom() * this.debristimejitter, this.debrisfadetime);
+}
+
+void func_breakable_colormod(entity this)
+{
+       float h;
+       if (!(this.spawnflags & BREAKABLE_INDICATE_DAMAGE))
+               return;
+       h = this.health / this.max_health;
+       if(h < 0.25)
+               this.colormod = '1 0 0';
+       else if(h <= 0.75)
+               this.colormod = '1 0 0' + '0 1 0' * (2 * h - 0.5);
+       else
+               this.colormod = '1 1 1';
+}
+
+void func_breakable_look_destroyed(entity this)
+{
+       float floorZ;
+
+       if(this.solid == SOLID_BSP) // in case a misc_follow moved me, save the current origin first
+               this.dropped_origin = this.origin;
+
+       if(this.mdl_dead == "")
+               this.effects |= EF_NODRAW;
+       else {
+               if (this.origin == '0 0 0')     {       // probably no origin brush, so don't spawn in the middle of the map..
+                       floorZ = this.absmin.z;
+                       setorigin(this, ((this.absmax + this.absmin) * 0.5));
+                       this.origin_z = floorZ;
+               }
+               _setmodel(this, this.mdl_dead);
+               ApplyMinMaxScaleAngles(this);
+               this.effects &= ~EF_NODRAW;
+       }
+
+       this.solid = SOLID_NOT;
+}
+
+void func_breakable_look_restore(entity this)
+{
+       _setmodel(this, this.mdl);
+       ApplyMinMaxScaleAngles(this);
+       this.effects &= ~EF_NODRAW;
+
+       if(this.mdl_dead != "") // only do this if we use mdl_dead, to behave better with misc_follow
+               setorigin(this, this.dropped_origin);
+
+       this.solid = SOLID_BSP;
+}
+
+void func_breakable_behave_destroyed(entity this)
+{
+       this.health = this.max_health;
+       this.takedamage = DAMAGE_NO;
+       if(this.bot_attack)
+               IL_REMOVE(g_bot_targets, this);
+       this.bot_attack = false;
+       this.event_damage = func_null;
+       this.state = STATE_BROKEN;
+       if(this.spawnflags & BREAKABLE_NODAMAGE)
+               this.use = func_null;
+       func_breakable_colormod(this);
+       if (this.noise1)
+               stopsound (this, CH_TRIGGER_SINGLE);
+}
+
+void func_breakable_think(entity this)
+{
+       this.nextthink = time;
+       CSQCMODEL_AUTOUPDATE(this);
+}
+
+void func_breakable_destroy(entity this, entity actor, entity trigger);
+void func_breakable_behave_restore(entity this)
+{
+       this.health = this.max_health;
+       if(this.sprite)
+       {
+               WaypointSprite_UpdateMaxHealth(this.sprite, this.max_health);
+               WaypointSprite_UpdateHealth(this.sprite, this.health);
+       }
+       if(!(this.spawnflags & BREAKABLE_NODAMAGE))
+       {
+               this.takedamage = DAMAGE_AIM;
+               if(!this.bot_attack)
+                       IL_PUSH(g_bot_targets, this);
+               this.bot_attack = true;
+               this.event_damage = func_breakable_damage;
+       }
+       if(this.spawnflags & BREAKABLE_NODAMAGE)
+               this.use = func_breakable_destroy; // don't need to set it usually, as .use isn't reset
+       this.state = STATE_ALIVE;
+       //this.nextthink = 0; // cancel auto respawn
+       setthink(this, func_breakable_think);
+       this.nextthink = time + 0.1;
+       func_breakable_colormod(this);
+       if (this.noise1)
+               _sound (this, CH_TRIGGER_SINGLE, this.noise1, VOL_BASE, ATTEN_NORM);
+}
+
+void func_breakable_init_for_player(entity this, entity player)
+{
+       if (this.noise1 && this.state == STATE_ALIVE && IS_REAL_CLIENT(player))
+       {
+               msg_entity = player;
+               soundto (MSG_ONE, this, CH_TRIGGER_SINGLE, this.noise1, VOL_BASE, ATTEN_NORM);
+       }
+}
+
+void func_breakable_destroyed(entity this)
+{
+       func_breakable_look_destroyed(this);
+       func_breakable_behave_destroyed(this);
+}
+
+void func_breakable_restore(entity this, entity actor, entity trigger)
+{
+       func_breakable_look_restore(this);
+       func_breakable_behave_restore(this);
+}
+
+void func_breakable_restore_self(entity this)
+{
+       func_breakable_restore(this, NULL, NULL);
+}
+
+vector debrisforce; // global, set before calling this
+void func_breakable_destroy(entity this, entity actor, entity trigger)
+{
+       float n, i;
+       string oldmsg;
+
+       entity act = this.owner;
+       this.owner = NULL; // set by W_PrepareExplosionByDamage
+
+       // now throw around the debris
+       n = tokenize_console(this.debris);
+       for(i = 0; i < n; ++i)
+               LaunchDebris(this, argv(i), debrisforce);
+
+       func_breakable_destroyed(this);
+
+       if(this.noise)
+               _sound (this, CH_TRIGGER, this.noise, VOL_BASE, ATTEN_NORM);
+
+       if(this.dmg)
+               RadiusDamage(this, act, this.dmg, this.dmg_edge, this.dmg_radius, this, NULL, this.dmg_force, DEATH_HURTTRIGGER.m_id, DMG_NOWEP, NULL);
+
+       if(this.cnt) // TODO
+               __pointparticles(this.cnt, this.absmin * 0.5 + this.absmax * 0.5, '0 0 0', this.count);
+
+       if(this.respawntime)
+       {
+               CSQCMODEL_AUTOUPDATE(this);
+               setthink(this, func_breakable_restore_self);
+               this.nextthink = time + this.respawntime + crandom() * this.respawntimejitter;
+       }
+
+       oldmsg = this.message;
+       this.message = "";
+       SUB_UseTargets(this, act, trigger);
+       this.message = oldmsg;
+}
+
+void func_breakable_destroy_self(entity this)
+{
+       func_breakable_destroy(this, NULL, NULL);
+}
+
+void func_breakable_damage(entity this, entity inflictor, entity attacker, float damage, int deathtype, .entity weaponentity, vector hitloc, vector force)
+{
+       if(this.state == STATE_BROKEN)
+               return;
+       if(this.spawnflags & NOSPLASH)
+               if(!(DEATH_ISSPECIAL(deathtype)) && (deathtype & HITTYPE_SPLASH))
+                       return;
+       if(this.team)
+               if(attacker.team == this.team)
+                       return;
+       this.pain_finished = time;
+       this.health = this.health - damage;
+       if(this.sprite)
+       {
+               WaypointSprite_Ping(this.sprite);
+               WaypointSprite_UpdateHealth(this.sprite, this.health);
+       }
+       func_breakable_colormod(this);
+
+       if(this.health <= 0)
+       {
+               debrisforce = force;
+
+               this.takedamage = DAMAGE_NO;
+               this.event_damage = func_null;
+
+               if(IS_CLIENT(attacker)) //&& this.classname == "func_assault_destructible")
+               {
+                       this.owner = attacker;
+                       this.realowner = attacker;
+               }
+
+               // do not explode NOW but in the NEXT FRAME!
+               // because recursive calls to RadiusDamage are not allowed
+               this.nextthink = time;
+               CSQCMODEL_AUTOUPDATE(this);
+               setthink(this, func_breakable_destroy_self);
+       }
+}
+
+void func_breakable_reset(entity this)
+{
+       this.team = this.team_saved;
+       func_breakable_look_restore(this);
+       if(this.spawnflags & START_DISABLED)
+               func_breakable_behave_destroyed(this);
+       else
+               func_breakable_behave_restore(this);
+}
+
+// destructible walls that can be used to trigger target_objective_decrease
+spawnfunc(func_breakable)
+{
+       float n, i;
+       if(!this.health)
+               this.health = 100;
+       this.max_health = this.health;
+
+       // yes, I know, MOVETYPE_NONE is not available here, not that one would want it here anyway
+       if(!this.debrismovetype) this.debrismovetype = MOVETYPE_BOUNCE;
+       if(!this.debrissolid) this.debrissolid = SOLID_NOT;
+       if(this.debrisvelocity == '0 0 0') this.debrisvelocity = '0 0 140';
+       if(this.debrisvelocityjitter == '0 0 0') this.debrisvelocityjitter = '70 70 70';
+       if(this.debrisavelocityjitter == '0 0 0') this.debrisavelocityjitter = '600 600 600';
+       if(!this.debristime) this.debristime = 3.5;
+       if(!this.debristimejitter) this.debristime = 2.5;
+
+       if(this.mdl != "")
+               this.cnt = _particleeffectnum(this.mdl);
+       if(this.count == 0)
+               this.count = 1;
+
+       if(this.message == "")
+               this.message = "got too close to an explosion";
+       if(this.message2 == "")
+               this.message2 = "was pushed into an explosion by";
+       if(!this.dmg_radius)
+               this.dmg_radius = 150;
+       if(!this.dmg_force)
+               this.dmg_force = 200;
+
+       this.mdl = this.model;
+       SetBrushEntityModel(this);
+
+       if(this.spawnflags & BREAKABLE_NODAMAGE)
+               this.use = func_breakable_destroy;
+       else
+               this.use = func_breakable_restore;
+
+       if(this.spawnflags & BREAKABLE_NODAMAGE)
+       {
+               this.takedamage = DAMAGE_NO;
+               this.event_damage = func_null;
+               this.bot_attack = false;
+       }
+
+       // precache all the models
+       if (this.mdl_dead)
+               precache_model(this.mdl_dead);
+       n = tokenize_console(this.debris);
+       for(i = 0; i < n; ++i)
+               precache_model(argv(i));
+       if(this.noise)
+               precache_sound(this.noise);
+       if(this.noise1)
+               precache_sound(this.noise1);
+
+       this.team_saved = this.team;
+       IL_PUSH(g_saved_team, this);
+       this.dropped_origin = this.origin;
+
+       this.reset = func_breakable_reset;
+       this.reset(this);
+
+       IL_PUSH(g_initforplayer, this);
+       this.init_for_player = func_breakable_init_for_player;
+
+       CSQCMODEL_AUTOINIT(this);
+}
+
+// for use in maps with a "model" key set
+spawnfunc(misc_breakablemodel) {
+       spawnfunc_func_breakable(this);
+}
+#endif
diff --git a/qcsrc/common/mapobjects/func/breakable.qh b/qcsrc/common/mapobjects/func/breakable.qh
new file mode 100644 (file)
index 0000000..0efbcfa
--- /dev/null
@@ -0,0 +1,12 @@
+#pragma once
+
+
+const int BREAKABLE_INDICATE_DAMAGE = BIT(1);
+const int BREAKABLE_NODAMAGE = BIT(2);
+
+const int STATE_ALIVE = 0;
+const int STATE_BROKEN = 1;
+
+#ifdef SVQC
+spawnfunc(func_breakable);
+#endif
diff --git a/qcsrc/common/mapobjects/func/button.qc b/qcsrc/common/mapobjects/func/button.qc
new file mode 100644 (file)
index 0000000..28e6481
--- /dev/null
@@ -0,0 +1,170 @@
+#include "button.qh"
+#ifdef SVQC
+// button and multiple button
+
+void button_wait(entity this);
+void button_return(entity this);
+
+void button_wait(entity this)
+{
+       this.state = STATE_TOP;
+       if(this.wait >= 0)
+       {
+               this.nextthink = this.ltime + this.wait;
+               setthink(this, button_return);
+       }
+       SUB_UseTargets(this, this.enemy, NULL);
+       this.frame = 1;                 // use alternate textures
+}
+
+void button_done(entity this)
+{
+       this.state = STATE_BOTTOM;
+}
+
+void button_return(entity this)
+{
+       this.state = STATE_DOWN;
+       SUB_CalcMove (this, this.pos1, TSPEED_LINEAR, this.speed, button_done);
+       this.frame = 0;                 // use normal textures
+       if (this.health)
+               this.takedamage = DAMAGE_YES;   // can be shot again
+}
+
+
+void button_blocked(entity this, entity blocker)
+{
+       // do nothing, just don't come all the way back out
+}
+
+
+void button_fire(entity this)
+{
+       this.health = this.max_health;
+       this.takedamage = DAMAGE_NO;    // will be reset upon return
+
+       if (this.state == STATE_UP || this.state == STATE_TOP)
+               return;
+
+       if (this.noise != "")
+               _sound (this, CH_TRIGGER, this.noise, VOL_BASE, ATTEN_NORM);
+
+       this.state = STATE_UP;
+       SUB_CalcMove (this, this.pos2, TSPEED_LINEAR, this.speed, button_wait);
+}
+
+void button_reset(entity this)
+{
+       this.health = this.max_health;
+       setorigin(this, this.pos1);
+       this.frame = 0;                 // use normal textures
+       this.state = STATE_BOTTOM;
+       this.velocity = '0 0 0';
+       setthink(this, func_null);
+       this.nextthink = 0;
+       if (this.health)
+               this.takedamage = DAMAGE_YES;   // can be shot again
+}
+
+void button_use(entity this, entity actor, entity trigger)
+{
+       if(this.active != ACTIVE_ACTIVE)
+               return;
+
+       this.enemy = actor;
+       button_fire(this);
+}
+
+void button_touch(entity this, entity toucher)
+{
+       if (!toucher)
+               return;
+       if (!toucher.iscreature)
+               return;
+       if(toucher.velocity * this.movedir < 0)
+               return;
+       this.enemy = toucher;
+       if (toucher.owner)
+               this.enemy = toucher.owner;
+       button_fire (this);
+}
+
+void button_damage(entity this, entity inflictor, entity attacker, float damage, int deathtype, .entity weaponentity, vector hitloc, vector force)
+{
+       if(this.spawnflags & NOSPLASH)
+               if(!(DEATH_ISSPECIAL(deathtype)) && (deathtype & HITTYPE_SPLASH))
+                       return;
+       if (this.spawnflags & BUTTON_DONTACCUMULATEDMG)
+       {
+               if (this.health <= damage)
+               {
+                       this.enemy = attacker;
+                       button_fire(this);
+               }
+       }
+       else
+       {
+               this.health = this.health - damage;
+               if (this.health <= 0)
+               {
+                       this.enemy = attacker;
+                       button_fire(this);
+               }
+       }
+}
+
+
+/*QUAKED spawnfunc_func_button (0 .5 .8) ?
+When a button is touched, it moves some distance in the direction of it's angle, triggers all of it's targets, waits some time, then returns to it's original position where it can be triggered again.
+
+"angle"                determines the opening direction
+"target"       all entities with a matching targetname will be used
+"speed"                override the default 40 speed
+"wait"         override the default 1 second wait (-1 = never return)
+"lip"          override the default 4 pixel lip remaining at end of move
+"health"       if set, the button must be killed instead of touched. If set to -1, the button will fire on ANY attack, even damageless ones like the InstaGib laser
+"noise"                sound that is played when the button is activated
+*/
+spawnfunc(func_button)
+{
+       SetMovedir(this);
+
+       if (!InitMovingBrushTrigger(this))
+               return;
+       this.effects |= EF_LOWPRECISION;
+
+       setblocked(this, button_blocked);
+       this.use = button_use;
+
+//     if (this.health == 0) // all buttons are now shootable
+//             this.health = 10;
+       if (this.health)
+       {
+               this.max_health = this.health;
+               this.event_damage = button_damage;
+               this.takedamage = DAMAGE_YES;
+       }
+       else
+               settouch(this, button_touch);
+
+       if (!this.speed)
+               this.speed = 40;
+       if (!this.wait)
+               this.wait = 1;
+       if (!this.lip)
+               this.lip = 4;
+
+    if(this.noise != "")
+        precache_sound(this.noise);
+
+       this.active = ACTIVE_ACTIVE;
+
+       this.pos1 = this.origin;
+       this.pos2 = this.pos1 + this.movedir*(fabs(this.movedir*this.size) - this.lip);
+    this.flags |= FL_NOTARGET;
+
+    this.reset = button_reset;
+
+       button_reset(this);
+}
+#endif
diff --git a/qcsrc/common/mapobjects/func/button.qh b/qcsrc/common/mapobjects/func/button.qh
new file mode 100644 (file)
index 0000000..86a0fc9
--- /dev/null
@@ -0,0 +1,4 @@
+#pragma once
+
+
+const int BUTTON_DONTACCUMULATEDMG = BIT(7);
diff --git a/qcsrc/common/mapobjects/func/conveyor.qc b/qcsrc/common/mapobjects/func/conveyor.qc
new file mode 100644 (file)
index 0000000..9ad326c
--- /dev/null
@@ -0,0 +1,178 @@
+#include "conveyor.qh"
+REGISTER_NET_LINKED(ENT_CLIENT_CONVEYOR)
+
+void conveyor_think(entity this)
+{
+#ifdef CSQC
+       // TODO: check if this is what is causing the glitchiness when switching between them
+       float dt = time - this.move_time;
+       this.move_time = time;
+       if(dt <= 0) { return; }
+#endif
+
+       // set myself as current conveyor where possible
+       IL_EACH(g_conveyed, it.conveyor == this,
+       {
+               it.conveyor = NULL;
+               IL_REMOVE(g_conveyed, it);
+       });
+
+       if(this.active == ACTIVE_ACTIVE)
+       {
+               FOREACH_ENTITY_RADIUS((this.absmin + this.absmax) * 0.5, vlen(this.absmax - this.absmin) * 0.5 + 1, it.conveyor.active == ACTIVE_NOT && isPushable(it),
+               {
+                       vector emin = it.absmin;
+                       vector emax = it.absmax;
+                       if(this.solid == SOLID_BSP)
+                       {
+                               emin -= '1 1 1';
+                               emax += '1 1 1';
+                       }
+                       if(boxesoverlap(emin, emax, this.absmin, this.absmax)) // quick
+                               if(WarpZoneLib_BoxTouchesBrush(emin, emax, this, it)) // accurate
+                               {
+                                       if(!it.conveyor)
+                                               IL_PUSH(g_conveyed, it);
+                                       it.conveyor = this;
+                               }
+               });
+
+               IL_EACH(g_conveyed, it.conveyor == this,
+               {
+                       if(IS_CLIENT(it)) // doing it via velocity has quite some advantages
+                               continue; // done in SV_PlayerPhysics   continue;
+
+                       setorigin(it, it.origin + this.movedir * PHYS_INPUT_FRAMETIME);
+                       move_out_of_solid(it);
+#ifdef SVQC
+                       UpdateCSQCProjectile(it);
+#endif
+                       /*
+                       // stupid conveyor code
+                       tracebox(it.origin, it.mins, it.maxs, it.origin + this.movedir * sys_frametime, MOVE_NORMAL, it);
+                       if(trace_fraction > 0)
+                               setorigin(it, trace_endpos);
+                       */
+               });
+       }
+
+#ifdef SVQC
+       this.nextthink = time;
+#endif
+}
+
+#ifdef SVQC
+
+bool conveyor_send(entity this, entity to, int sendflags)
+{
+       WriteHeader(MSG_ENTITY, ENT_CLIENT_CONVEYOR);
+       WriteByte(MSG_ENTITY, sendflags);
+
+       if(sendflags & SF_TRIGGER_INIT)
+       {
+               WriteByte(MSG_ENTITY, this.warpzone_isboxy);
+               WriteVector(MSG_ENTITY, this.origin);
+
+               WriteVector(MSG_ENTITY, this.mins);
+               WriteVector(MSG_ENTITY, this.maxs);
+
+               WriteVector(MSG_ENTITY, this.movedir);
+
+               WriteByte(MSG_ENTITY, this.speed);
+               WriteByte(MSG_ENTITY, this.active);
+
+               WriteString(MSG_ENTITY, this.targetname);
+               WriteString(MSG_ENTITY, this.target);
+       }
+
+       if(sendflags & SF_TRIGGER_UPDATE)
+               WriteByte(MSG_ENTITY, this.active);
+
+       return true;
+}
+
+void conveyor_init(entity this)
+{
+       if (!this.speed) this.speed = 200;
+       this.movedir *= this.speed;
+       setthink(this, conveyor_think);
+       this.nextthink = time;
+       this.setactive = generic_netlinked_setactive;
+       IFTARGETED
+       {
+               // backwards compatibility
+               this.use = generic_netlinked_legacy_use;
+       }
+       this.reset = generic_netlinked_reset;
+       this.reset(this);
+
+       FixSize(this);
+
+       Net_LinkEntity(this, 0, false, conveyor_send);
+
+       this.SendFlags |= SF_TRIGGER_INIT;
+}
+
+spawnfunc(trigger_conveyor)
+{
+       SetMovedir(this);
+       EXACTTRIGGER_INIT;
+       conveyor_init(this);
+}
+
+spawnfunc(func_conveyor)
+{
+       SetMovedir(this);
+       InitMovingBrushTrigger(this);
+       set_movetype(this, MOVETYPE_NONE);
+       conveyor_init(this);
+}
+
+#elif defined(CSQC)
+
+void conveyor_draw(entity this) { conveyor_think(this); }
+
+void conveyor_init(entity this, bool isnew)
+{
+       if(isnew)
+               IL_PUSH(g_drawables, this);
+       this.draw = conveyor_draw;
+       this.drawmask = MASK_NORMAL;
+
+       set_movetype(this, MOVETYPE_NONE);
+       this.model = "";
+       this.solid = SOLID_TRIGGER;
+       this.move_time = time;
+}
+
+NET_HANDLE(ENT_CLIENT_CONVEYOR, bool isnew)
+{
+       int sendflags = ReadByte();
+
+       if(sendflags & SF_TRIGGER_INIT)
+       {
+               this.warpzone_isboxy = ReadByte();
+               this.origin = ReadVector();
+               setorigin(this, this.origin);
+
+               this.mins = ReadVector();
+               this.maxs = ReadVector();
+               setsize(this, this.mins, this.maxs);
+
+               this.movedir = ReadVector();
+
+               this.speed = ReadByte();
+               this.active = ReadByte();
+
+               this.targetname = strzone(ReadString());
+               this.target = strzone(ReadString());
+
+               conveyor_init(this, isnew);
+       }
+
+       if(sendflags & SF_TRIGGER_UPDATE)
+               this.active = ReadByte();
+
+       return true;
+}
+#endif
diff --git a/qcsrc/common/mapobjects/func/conveyor.qh b/qcsrc/common/mapobjects/func/conveyor.qh
new file mode 100644 (file)
index 0000000..22b674f
--- /dev/null
@@ -0,0 +1,5 @@
+#pragma once
+#include "../defs.qh"
+
+IntrusiveList g_conveyed;
+STATIC_INIT(g_conveyed) { g_conveyed = IL_NEW(); }
diff --git a/qcsrc/common/mapobjects/func/door.qc b/qcsrc/common/mapobjects/func/door.qc
new file mode 100644 (file)
index 0000000..c19041a
--- /dev/null
@@ -0,0 +1,801 @@
+#include "door.qh"
+#include "door_rotating.qh"
+/*
+
+Doors are similar to buttons, but can spawn a fat trigger field around them
+to open without a touch, and they link together to form simultanious
+double/quad doors.
+
+Door.owner is the master door.  If there is only one door, it points to itself.
+If multiple doors, all will point to a single one.
+
+Door.enemy chains from the master door through all doors linked in the chain.
+
+*/
+
+
+/*
+=============================================================================
+
+THINK FUNCTIONS
+
+=============================================================================
+*/
+
+void door_go_down(entity this);
+void door_go_up(entity this, entity actor, entity trigger);
+
+void door_blocked(entity this, entity blocker)
+{
+       if((this.spawnflags & DOOR_CRUSH)
+#ifdef SVQC
+               && (blocker.takedamage != DAMAGE_NO)
+#elif defined(CSQC)
+               && !IS_DEAD(blocker)
+#endif
+       )
+       { // KIll Kill Kill!!
+#ifdef SVQC
+               Damage (blocker, this, this, 10000, DEATH_HURTTRIGGER.m_id, DMG_NOWEP, blocker.origin, '0 0 0');
+#endif
+       }
+       else
+       {
+#ifdef SVQC
+               if((this.dmg) && (blocker.takedamage == DAMAGE_YES))    // Shall we bite?
+                       Damage (blocker, this, this, this.dmg, DEATH_HURTTRIGGER.m_id, DMG_NOWEP, blocker.origin, '0 0 0');
+#endif
+
+                // don't change direction for dead or dying stuff
+               if(IS_DEAD(blocker)
+#ifdef SVQC
+                       && (blocker.takedamage == DAMAGE_NO)
+#endif
+               )
+               {
+                       if (this.wait >= 0)
+                       {
+                               if (this.state == STATE_DOWN)
+                               {
+                                       if (this.classname == "door")
+                                               door_go_up(this, NULL, NULL);
+                                       else
+                                               door_rotating_go_up(this, blocker);
+                               }
+                               else
+                               {
+                                       if (this.classname == "door")
+                                               door_go_down(this);
+                                       else
+                                               door_rotating_go_down(this);
+                               }
+                       }
+               }
+#ifdef SVQC
+               else
+               {
+                       //gib dying stuff just to make sure
+                       if((this.dmg) && (blocker.takedamage != DAMAGE_NO))    // Shall we bite?
+                               Damage (blocker, this, this, 10000, DEATH_HURTTRIGGER.m_id, DMG_NOWEP, blocker.origin, '0 0 0');
+               }
+#endif
+       }
+}
+
+void door_hit_top(entity this)
+{
+       if (this.noise1 != "")
+               _sound (this, CH_TRIGGER_SINGLE, this.noise1, VOL_BASE, ATTEN_NORM);
+       this.state = STATE_TOP;
+       if (this.spawnflags & DOOR_TOGGLE)
+               return;         // don't come down automatically
+       if (this.classname == "door")
+       {
+               setthink(this, door_go_down);
+       } else
+       {
+               setthink(this, door_rotating_go_down);
+       }
+       this.nextthink = this.ltime + this.wait;
+}
+
+void door_hit_bottom(entity this)
+{
+       if (this.noise1 != "")
+               _sound (this, CH_TRIGGER_SINGLE, this.noise1, VOL_BASE, ATTEN_NORM);
+       this.state = STATE_BOTTOM;
+}
+
+void door_go_down(entity this)
+{
+       if (this.noise2 != "")
+               _sound (this, CH_TRIGGER_SINGLE, this.noise2, VOL_BASE, ATTEN_NORM);
+       if (this.max_health)
+       {
+               this.takedamage = DAMAGE_YES;
+               this.health = this.max_health;
+       }
+
+       this.state = STATE_DOWN;
+       SUB_CalcMove (this, this.pos1, TSPEED_LINEAR, this.speed, door_hit_bottom);
+}
+
+void door_go_up(entity this, entity actor, entity trigger)
+{
+       if (this.state == STATE_UP)
+               return;         // already going up
+
+       if (this.state == STATE_TOP)
+       {       // reset top wait time
+               this.nextthink = this.ltime + this.wait;
+               return;
+       }
+
+       if (this.noise2 != "")
+               _sound (this, CH_TRIGGER_SINGLE, this.noise2, VOL_BASE, ATTEN_NORM);
+       this.state = STATE_UP;
+       SUB_CalcMove (this, this.pos2, TSPEED_LINEAR, this.speed, door_hit_top);
+
+       string oldmessage;
+       oldmessage = this.message;
+       this.message = "";
+       SUB_UseTargets(this, actor, trigger);
+       this.message = oldmessage;
+}
+
+
+/*
+=============================================================================
+
+ACTIVATION FUNCTIONS
+
+=============================================================================
+*/
+
+bool door_check_keys(entity door, entity player)
+{
+       if(door.owner)
+               door = door.owner;
+
+       // no key needed
+       if(!door.itemkeys)
+               return true;
+
+       // this door require a key
+       // only a player can have a key
+       if(!IS_PLAYER(player))
+               return false;
+
+       entity store = player;
+#ifdef SVQC
+       store = PS(player);
+#endif
+       int valid = (door.itemkeys & store.itemkeys);
+       door.itemkeys &= ~valid; // only some of the needed keys were given
+
+       if(!door.itemkeys)
+       {
+#ifdef SVQC
+               play2(player, door.noise);
+               Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_DOOR_UNLOCKED);
+#endif
+               return true;
+       }
+
+       if(!valid)
+       {
+#ifdef SVQC
+               if(player.key_door_messagetime <= time)
+               {
+                       play2(player, door.noise3);
+                       Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_DOOR_LOCKED_NEED, item_keys_keylist(door.itemkeys));
+                       player.key_door_messagetime = time + 2;
+               }
+#endif
+               return false;
+       }
+
+       // door needs keys the player doesn't have
+#ifdef SVQC
+       if(player.key_door_messagetime <= time)
+       {
+               play2(player, door.noise3);
+               Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_DOOR_LOCKED_ALSONEED, item_keys_keylist(door.itemkeys));
+               player.key_door_messagetime = time + 2;
+       }
+#endif
+
+       return false;
+}
+
+void door_fire(entity this, entity actor, entity trigger)
+{
+       if (this.owner != this)
+               objerror (this, "door_fire: this.owner != this");
+
+       if (this.spawnflags & DOOR_TOGGLE)
+       {
+               if (this.state == STATE_UP || this.state == STATE_TOP)
+               {
+                       entity e = this;
+                       do {
+                               if (e.classname == "door") {
+                                       door_go_down(e);
+                               } else {
+                                       door_rotating_go_down(e);
+                               }
+                               e = e.enemy;
+                       } while ((e != this) && (e != NULL));
+                       return;
+               }
+       }
+
+// trigger all paired doors
+       entity e = this;
+       do {
+               if (e.classname == "door") {
+                       door_go_up(e, actor, trigger);
+               } else {
+                       // if the BIDIR spawnflag (==2) is set and the trigger has set trigger_reverse, reverse the opening direction
+                       if ((e.spawnflags & DOOR_ROTATING_BIDIR) && trigger.trigger_reverse!=0 && e.lip != 666 && e.state == STATE_BOTTOM) {
+                               e.lip = 666; // e.lip is used to remember reverse opening direction for door_rotating
+                               e.pos2 = '0 0 0' - e.pos2;
+                       }
+                       // if BIDIR_IN_DOWN (==8) is set, prevent the door from reoping during closing if it is triggered from the wrong side
+                       if (!((e.spawnflags & DOOR_ROTATING_BIDIR) &&  (e.spawnflags & DOOR_ROTATING_BIDIR_IN_DOWN) && e.state == STATE_DOWN
+                               && (((e.lip == 666) && (trigger.trigger_reverse == 0)) || ((e.lip != 666) && (trigger.trigger_reverse != 0)))))
+                       {
+                               door_rotating_go_up(e, trigger);
+                       }
+               }
+               e = e.enemy;
+       } while ((e != this) && (e != NULL));
+}
+
+void door_use(entity this, entity actor, entity trigger)
+{
+       //dprint("door_use (model: ");dprint(this.model);dprint(")\n");
+
+       if (this.owner)
+               door_fire(this.owner, actor, trigger);
+}
+
+void door_damage(entity this, entity inflictor, entity attacker, float damage, int deathtype, .entity weaponentity, vector hitloc, vector force)
+{
+       if(this.spawnflags & NOSPLASH)
+               if(!(DEATH_ISSPECIAL(deathtype)) && (deathtype & HITTYPE_SPLASH))
+                       return;
+       this.health = this.health - damage;
+
+       if (this.itemkeys)
+       {
+               // don't allow opening doors through damage if keys are required
+               return;
+       }
+
+       if (this.health <= 0)
+       {
+               this.owner.health = this.owner.max_health;
+               this.owner.takedamage = DAMAGE_NO;      // wil be reset upon return
+               door_use(this.owner, NULL, NULL);
+       }
+}
+
+.float door_finished;
+
+/*
+================
+door_touch
+
+Prints messages
+================
+*/
+
+void door_touch(entity this, entity toucher)
+{
+       if (!IS_PLAYER(toucher))
+               return;
+       if (this.owner.door_finished > time)
+               return;
+
+       this.owner.door_finished = time + 2;
+
+#ifdef SVQC
+       if (!(this.owner.dmg) && (this.owner.message != ""))
+       {
+               if (IS_CLIENT(toucher))
+                       centerprint(toucher, this.owner.message);
+               play2(toucher, this.owner.noise);
+       }
+#endif
+}
+
+void door_generic_plat_blocked(entity this, entity blocker)
+{
+       if((this.spawnflags & DOOR_CRUSH) && (blocker.takedamage != DAMAGE_NO)) { // Kill Kill Kill!!
+#ifdef SVQC
+               Damage (blocker, this, this, 10000, DEATH_HURTTRIGGER.m_id, DMG_NOWEP, blocker.origin, '0 0 0');
+#endif
+       }
+       else
+       {
+
+#ifdef SVQC
+               if((this.dmg) && (blocker.takedamage == DAMAGE_YES))    // Shall we bite?
+                       Damage (blocker, this, this, this.dmg, DEATH_HURTTRIGGER.m_id, DMG_NOWEP, blocker.origin, '0 0 0');
+#endif
+
+                //Dont chamge direction for dead or dying stuff
+               if(IS_DEAD(blocker) && (blocker.takedamage == DAMAGE_NO))
+               {
+                       if (this.wait >= 0)
+                       {
+                               if (this.state == STATE_DOWN)
+                                       door_rotating_go_up (this, blocker);
+                               else
+                                       door_rotating_go_down (this);
+                       }
+               }
+#ifdef SVQC
+               else
+               {
+                       //gib dying stuff just to make sure
+                       if((this.dmg) && (blocker.takedamage != DAMAGE_NO))    // Shall we bite?
+                               Damage (blocker, this, this, 10000, DEATH_HURTTRIGGER.m_id, DMG_NOWEP, blocker.origin, '0 0 0');
+               }
+#endif
+       }
+}
+
+/*
+=========================================
+door trigger
+
+Spawned if a door lacks a real activator
+=========================================
+*/
+
+void door_trigger_touch(entity this, entity toucher)
+{
+       if (toucher.health < 1)
+#ifdef SVQC
+               if (!((toucher.iscreature || (toucher.flags & FL_PROJECTILE)) && !IS_DEAD(toucher)))
+#elif defined(CSQC)
+               if(!((IS_CLIENT(toucher) || toucher.classname == "csqcprojectile") && !IS_DEAD(toucher)))
+#endif
+                       return;
+
+       if (time < this.door_finished)
+               return;
+
+       // check if door is locked
+       if (!door_check_keys(this, toucher))
+               return;
+
+       this.door_finished = time + 1;
+
+       door_use(this.owner, toucher, NULL);
+}
+
+void door_spawnfield(entity this, vector fmins, vector fmaxs)
+{
+       entity  trigger;
+       vector  t1 = fmins, t2 = fmaxs;
+
+       trigger = new(doortriggerfield);
+       set_movetype(trigger, MOVETYPE_NONE);
+       trigger.solid = SOLID_TRIGGER;
+       trigger.owner = this;
+#ifdef SVQC
+       settouch(trigger, door_trigger_touch);
+#endif
+
+       setsize (trigger, t1 - '60 60 8', t2 + '60 60 8');
+}
+
+
+/*
+=============
+LinkDoors
+
+
+=============
+*/
+
+entity LinkDoors_nextent(entity cur, entity near, entity pass)
+{
+       while((cur = find(cur, classname, pass.classname)) && ((cur.spawnflags & DOOR_DONT_LINK) || cur.enemy))
+       {
+       }
+       return cur;
+}
+
+bool LinkDoors_isconnected(entity e1, entity e2, entity pass)
+{
+       float DELTA = 4;
+       if((e1.absmin_x > e2.absmax_x + DELTA)
+       || (e1.absmin_y > e2.absmax_y + DELTA)
+       || (e1.absmin_z > e2.absmax_z + DELTA)
+       || (e2.absmin_x > e1.absmax_x + DELTA)
+       || (e2.absmin_y > e1.absmax_y + DELTA)
+       || (e2.absmin_z > e1.absmax_z + DELTA)
+       ) { return false; }
+       return true;
+}
+
+#ifdef SVQC
+void door_link();
+#endif
+void LinkDoors(entity this)
+{
+       entity  t;
+       vector  cmins, cmaxs;
+
+#ifdef SVQC
+       door_link();
+#endif
+
+       if (this.enemy)
+               return;         // already linked by another door
+       if (this.spawnflags & DOOR_DONT_LINK)
+       {
+               this.owner = this.enemy = this;
+
+               if (this.health)
+                       return;
+               IFTARGETED
+                       return;
+               if (this.items)
+                       return;
+
+               door_spawnfield(this, this.absmin, this.absmax);
+
+               return;         // don't want to link this door
+       }
+
+       FindConnectedComponent(this, enemy, LinkDoors_nextent, LinkDoors_isconnected, this);
+
+       // set owner, and make a loop of the chain
+       LOG_TRACE("LinkDoors: linking doors:");
+       for(t = this; ; t = t.enemy)
+       {
+               LOG_TRACE(" ", etos(t));
+               t.owner = this;
+               if(t.enemy == NULL)
+               {
+                       t.enemy = this;
+                       break;
+               }
+       }
+       LOG_TRACE("");
+
+       // collect health, targetname, message, size
+       cmins = this.absmin;
+       cmaxs = this.absmax;
+       for(t = this; ; t = t.enemy)
+       {
+               if(t.health && !this.health)
+                       this.health = t.health;
+               if((t.targetname != "") && (this.targetname == ""))
+                       this.targetname = t.targetname;
+               if((t.message != "") && (this.message == ""))
+                       this.message = t.message;
+               if (t.absmin_x < cmins_x)
+                       cmins_x = t.absmin_x;
+               if (t.absmin_y < cmins_y)
+                       cmins_y = t.absmin_y;
+               if (t.absmin_z < cmins_z)
+                       cmins_z = t.absmin_z;
+               if (t.absmax_x > cmaxs_x)
+                       cmaxs_x = t.absmax_x;
+               if (t.absmax_y > cmaxs_y)
+                       cmaxs_y = t.absmax_y;
+               if (t.absmax_z > cmaxs_z)
+                       cmaxs_z = t.absmax_z;
+               if(t.enemy == this)
+                       break;
+       }
+
+       // distribute health, targetname, message
+       for(t = this; t; t = t.enemy)
+       {
+               t.health = this.health;
+               t.targetname = this.targetname;
+               t.message = this.message;
+               if(t.enemy == this)
+                       break;
+       }
+
+       // shootable, or triggered doors just needed the owner/enemy links,
+       // they don't spawn a field
+
+       if (this.health)
+               return;
+       IFTARGETED
+               return;
+       if (this.items)
+               return;
+
+       door_spawnfield(this, cmins, cmaxs);
+}
+
+REGISTER_NET_LINKED(ENT_CLIENT_DOOR)
+
+#ifdef SVQC
+/*QUAKED spawnfunc_func_door (0 .5 .8) ? START_OPEN x DOOR_DONT_LINK GOLD_KEY SILVER_KEY TOGGLE
+if two doors touch, they are assumed to be connected and operate as a unit.
+
+TOGGLE causes the door to wait in both the start and end states for a trigger event.
+
+START_OPEN causes the door to move to its destination when spawned, and operate in reverse.  It is used to temporarily or permanently close off an area when triggered (not useful for touch or takedamage doors).
+
+GOLD_KEY causes the door to open only if the activator holds a gold key.
+
+SILVER_KEY causes the door to open only if the activator holds a silver key.
+
+"message"      is printed when the door is touched if it is a trigger door and it hasn't been fired yet
+"angle"                determines the opening direction
+"targetname" if set, no touch field will be spawned and a remote button or trigger field activates the door.
+"health"       if set, door must be shot open
+"speed"                movement speed (100 default)
+"wait"         wait before returning (3 default, -1 = never return)
+"lip"          lip remaining at end of move (8 default)
+"dmg"          damage to inflict when blocked (2 default)
+"sounds"
+0)     no sound
+1)     stone
+2)     base
+3)     stone chain
+4)     screechy metal
+FIXME: only one sound set available at the time being
+
+*/
+
+float door_send(entity this, entity to, float sf)
+{
+       WriteHeader(MSG_ENTITY, ENT_CLIENT_DOOR);
+       WriteByte(MSG_ENTITY, sf);
+
+       if(sf & SF_TRIGGER_INIT)
+       {
+               WriteString(MSG_ENTITY, this.classname);
+               WriteByte(MSG_ENTITY, this.spawnflags);
+
+               WriteString(MSG_ENTITY, this.model);
+
+               trigger_common_write(this, true);
+
+               WriteVector(MSG_ENTITY, this.pos1);
+               WriteVector(MSG_ENTITY, this.pos2);
+
+               WriteVector(MSG_ENTITY, this.size);
+
+               WriteShort(MSG_ENTITY, this.wait);
+               WriteShort(MSG_ENTITY, this.speed);
+               WriteByte(MSG_ENTITY, this.lip);
+               WriteByte(MSG_ENTITY, this.state);
+               WriteCoord(MSG_ENTITY, this.ltime);
+       }
+
+       if(sf & SF_TRIGGER_RESET)
+       {
+               // client makes use of this, we do not
+       }
+
+       if(sf & SF_TRIGGER_UPDATE)
+       {
+               WriteVector(MSG_ENTITY, this.origin);
+
+               WriteVector(MSG_ENTITY, this.pos1);
+               WriteVector(MSG_ENTITY, this.pos2);
+       }
+
+       return true;
+}
+
+void door_link()
+{
+       // set size now, as everything is loaded
+       //FixSize(this);
+       //Net_LinkEntity(this, false, 0, door_send);
+}
+#endif
+
+void door_init_startopen(entity this)
+{
+       setorigin(this, this.pos2);
+       this.pos2 = this.pos1;
+       this.pos1 = this.origin;
+
+#ifdef SVQC
+       this.SendFlags |= SF_TRIGGER_UPDATE;
+#endif
+}
+
+void door_reset(entity this)
+{
+       setorigin(this, this.pos1);
+       this.velocity = '0 0 0';
+       this.state = STATE_BOTTOM;
+       setthink(this, func_null);
+       this.nextthink = 0;
+
+#ifdef SVQC
+       this.SendFlags |= SF_TRIGGER_RESET;
+#endif
+}
+
+#ifdef SVQC
+
+// common code for func_door and func_door_rotating spawnfuncs
+void door_init_shared(entity this)
+{
+       this.max_health = this.health;
+
+       // unlock sound
+       if(this.noise == "")
+       {
+               this.noise = "misc/talk.wav";
+       }
+       // door still locked sound
+       if(this.noise3 == "")
+       {
+               this.noise3 = "misc/talk.wav";
+       }
+       precache_sound(this.noise);
+       precache_sound(this.noise3);
+
+       if((this.dmg || (this.spawnflags & DOOR_CRUSH)) && (this.message == ""))
+       {
+               this.message = "was squished";
+       }
+       if((this.dmg || (this.spawnflags & DOOR_CRUSH)) && (this.message2 == ""))
+       {
+               this.message2 = "was squished by";
+       }
+
+       // TODO: other soundpacks
+       if (this.sounds > 0)
+       {
+               this.noise2 = "plats/medplat1.wav";
+               this.noise1 = "plats/medplat2.wav";
+       }
+
+       // sound when door stops moving
+       if(this.noise1 && this.noise1 != "")
+       {
+               precache_sound(this.noise1);
+       }
+       // sound when door is moving
+       if(this.noise2 && this.noise2 != "")
+       {
+               precache_sound(this.noise2);
+       }
+
+       if (!this.wait)
+       {
+               this.wait = 3;
+       }
+       if (!this.lip)
+       {
+               this.lip = 8;
+       }
+
+       this.state = STATE_BOTTOM;
+
+       if (this.health)
+       {
+               //this.canteamdamage = true; // TODO
+               this.takedamage = DAMAGE_YES;
+               this.event_damage = door_damage;
+       }
+
+       if (this.items)
+       {
+               this.wait = -1;
+       }
+}
+
+// spawnflags require key (for now only func_door)
+spawnfunc(func_door)
+{
+       // Quake 1 keys compatibility
+       if (this.spawnflags & SPAWNFLAGS_GOLD_KEY)
+               this.itemkeys |= ITEM_KEY_BIT(0);
+       if (this.spawnflags & SPAWNFLAGS_SILVER_KEY)
+               this.itemkeys |= ITEM_KEY_BIT(1);
+
+       SetMovedir(this);
+
+       if (!InitMovingBrushTrigger(this))
+               return;
+       this.effects |= EF_LOWPRECISION;
+       this.classname = "door";
+
+       setblocked(this, door_blocked);
+       this.use = door_use;
+
+       this.pos1 = this.origin;
+       this.pos2 = this.pos1 + this.movedir*(fabs(this.movedir*this.size) - this.lip);
+
+       if(this.spawnflags & DOOR_NONSOLID)
+               this.solid = SOLID_NOT;
+
+// DOOR_START_OPEN is to allow an entity to be lighted in the closed position
+// but spawn in the open position
+       if (this.spawnflags & DOOR_START_OPEN)
+               InitializeEntity(this, door_init_startopen, INITPRIO_SETLOCATION);
+
+       door_init_shared(this);
+
+       if (!this.speed)
+       {
+               this.speed = 100;
+       }
+
+       settouch(this, door_touch);
+
+// LinkDoors can't be done until all of the doors have been spawned, so
+// the sizes can be detected properly.
+       InitializeEntity(this, LinkDoors, INITPRIO_LINKDOORS);
+
+       this.reset = door_reset;
+}
+
+#elif defined(CSQC)
+
+NET_HANDLE(ENT_CLIENT_DOOR, bool isnew)
+{
+       int sf = ReadByte();
+
+       if(sf & SF_TRIGGER_INIT)
+       {
+               this.classname = strzone(ReadString());
+               this.spawnflags = ReadByte();
+
+               this.mdl = strzone(ReadString());
+               _setmodel(this, this.mdl);
+
+               trigger_common_read(this, true);
+
+               this.pos1 = ReadVector();
+               this.pos2 = ReadVector();
+
+               this.size = ReadVector();
+
+               this.wait = ReadShort();
+               this.speed = ReadShort();
+               this.lip = ReadByte();
+               this.state = ReadByte();
+               this.ltime = ReadCoord();
+
+               this.solid = SOLID_BSP;
+               set_movetype(this, MOVETYPE_PUSH);
+               this.use = door_use;
+
+               LinkDoors(this);
+
+               if(this.spawnflags & DOOR_START_OPEN)
+                       door_init_startopen(this);
+
+               this.move_time = time;
+               set_movetype(this, MOVETYPE_PUSH);
+       }
+
+       if(sf & SF_TRIGGER_RESET)
+       {
+               door_reset(this);
+       }
+
+       if(sf & SF_TRIGGER_UPDATE)
+       {
+               this.origin = ReadVector();
+               setorigin(this, this.origin);
+
+               this.pos1 = ReadVector();
+               this.pos2 = ReadVector();
+       }
+       return true;
+}
+
+#endif
diff --git a/qcsrc/common/mapobjects/func/door.qh b/qcsrc/common/mapobjects/func/door.qh
new file mode 100644 (file)
index 0000000..181de8b
--- /dev/null
@@ -0,0 +1,18 @@
+#pragma once
+
+
+const int DOOR_START_OPEN = BIT(0);
+const int DOOR_DONT_LINK = BIT(2);
+const int SPAWNFLAGS_GOLD_KEY = BIT(3); // Quake 1 compat, can only be used with func_door!
+const int SPAWNFLAGS_SILVER_KEY = BIT(4); // Quake 1 compat, can only be used with func_door!
+const int DOOR_TOGGLE = BIT(5);
+
+const int DOOR_NONSOLID = BIT(10);
+const int DOOR_CRUSH = BIT(11); // can't use CRUSH cause that is the same as DOOR_DONT_LINK
+
+
+#ifdef CSQC
+// stuff for preload
+
+.float door_finished;
+#endif
diff --git a/qcsrc/common/mapobjects/func/door_rotating.qc b/qcsrc/common/mapobjects/func/door_rotating.qc
new file mode 100644 (file)
index 0000000..41fd05e
--- /dev/null
@@ -0,0 +1,159 @@
+#include "door_rotating.qh"
+/*QUAKED spawnfunc_func_door_rotating (0 .5 .8) ? START_OPEN BIDIR DOOR_DONT_LINK BIDIR_IN_DOWN x TOGGLE X_AXIS Y_AXIS
+if two doors touch, they are assumed to be connected and operate as a unit.
+
+TOGGLE causes the door to wait in both the start and end states for a trigger event.
+
+BIDIR makes the door work bidirectional, so that the opening direction is always away from the requestor.
+The usage of bidirectional doors requires two manually instantiated triggers (trigger_multiple), the one to open it in the other direction
+must have set trigger_reverse to 1.
+BIDIR_IN_DOWN will the door prevent from reopening while closing if it is triggered from the other side.
+
+START_OPEN causes the door to move to its destination when spawned, and operate in reverse.  It is used to temporarily or permanently close off an area when triggered (not usefull for touch or takedamage doors).
+
+"message"      is printed when the door is touched if it is a trigger door and it hasn't been fired yet
+"angle"                determines the destination angle for opening. negative values reverse the direction.
+"targetname"    if set, no touch field will be spawned and a remote button or trigger field activates the door.
+"health"       if set, door must be shot open
+"speed"                movement speed (100 default)
+"wait"         wait before returning (3 default, -1 = never return)
+"dmg"          damage to inflict when blocked (2 default)
+"sounds"
+0)     no sound
+1)     stone
+2)     base
+3)     stone chain
+4)     screechy metal
+FIXME: only one sound set available at the time being
+*/
+
+#ifdef GAMEQC
+void door_rotating_hit_top(entity this)
+{
+       if (this.noise1 != "")
+               _sound (this, CH_TRIGGER_SINGLE, this.noise1, VOL_BASE, ATTEN_NORM);
+       this.state = STATE_TOP;
+       if (this.spawnflags & DOOR_TOGGLE)
+               return;         // don't come down automatically
+       setthink(this, door_rotating_go_down);
+       this.nextthink = this.ltime + this.wait;
+}
+
+void door_rotating_hit_bottom(entity this)
+{
+       if (this.noise1 != "")
+               _sound (this, CH_TRIGGER_SINGLE, this.noise1, VOL_BASE, ATTEN_NORM);
+       if (this.lip==666) // this.lip is used to remember reverse opening direction for door_rotating
+       {
+               this.pos2 = '0 0 0' - this.pos2;
+               this.lip = 0;
+       }
+       this.state = STATE_BOTTOM;
+}
+
+void door_rotating_go_down(entity this)
+{
+       if (this.noise2 != "")
+               _sound (this, CH_TRIGGER_SINGLE, this.noise2, VOL_BASE, ATTEN_NORM);
+       if (this.max_health)
+       {
+               this.takedamage = DAMAGE_YES;
+               this.health = this.max_health;
+       }
+
+       this.state = STATE_DOWN;
+       SUB_CalcAngleMove (this, this.pos1, TSPEED_LINEAR, this.speed, door_rotating_hit_bottom);
+}
+
+void door_rotating_go_up(entity this, entity oth)
+{
+       if (this.state == STATE_UP)
+               return;         // already going up
+
+       if (this.state == STATE_TOP)
+       {       // reset top wait time
+               this.nextthink = this.ltime + this.wait;
+               return;
+       }
+       if (this.noise2 != "")
+               _sound (this, CH_TRIGGER_SINGLE, this.noise2, VOL_BASE, ATTEN_NORM);
+       this.state = STATE_UP;
+       SUB_CalcAngleMove (this, this.pos2, TSPEED_LINEAR, this.speed, door_rotating_hit_top);
+
+       string oldmessage;
+       oldmessage = this.message;
+       this.message = "";
+       SUB_UseTargets(this, NULL, oth); // TODO: is oth needed here?
+       this.message = oldmessage;
+}
+#endif
+
+#ifdef SVQC
+void door_rotating_reset(entity this)
+{
+       this.angles = this.pos1;
+       this.avelocity = '0 0 0';
+       this.state = STATE_BOTTOM;
+       setthink(this, func_null);
+       this.nextthink = 0;
+}
+
+void door_rotating_init_startopen(entity this)
+{
+       this.angles = this.movedir;
+       this.pos2 = '0 0 0';
+       this.pos1 = this.movedir;
+}
+
+spawnfunc(func_door_rotating)
+{
+       //if (!this.deathtype) // map makers can override this
+       //      this.deathtype = " got in the way";
+
+       // I abuse "movedir" for denoting the axis for now
+       if (this.spawnflags & DOOR_ROTATING_XAXIS)
+               this.movedir = '0 0 1';
+       else if (this.spawnflags & DOOR_ROTATING_YAXIS)
+               this.movedir = '1 0 0';
+       else // Z
+               this.movedir = '0 1 0';
+
+       if (this.angles_y==0) this.angles_y = 90;
+
+       this.movedir = this.movedir * this.angles_y;
+       this.angles = '0 0 0';
+
+       this.avelocity = this.movedir;
+       if (!InitMovingBrushTrigger(this))
+               return;
+       this.velocity = '0 0 0';
+       //this.effects |= EF_LOWPRECISION;
+       this.classname = "door_rotating";
+
+       setblocked(this, door_blocked);
+       this.use = door_use;
+
+       this.pos1 = '0 0 0';
+       this.pos2 = this.movedir;
+
+// DOOR_START_OPEN is to allow an entity to be lighted in the closed position
+// but spawn in the open position
+       if (this.spawnflags & DOOR_START_OPEN)
+               InitializeEntity(this, door_rotating_init_startopen, INITPRIO_SETLOCATION);
+
+       door_init_shared(this);
+       if (!this.speed)
+       {
+               this.speed = 50;
+       }
+       this.lip = 0; // this.lip is used to remember reverse opening direction for door_rotating
+
+       settouch(this, door_touch);
+
+// LinkDoors can't be done until all of the doors have been spawned, so
+// the sizes can be detected properly.
+       InitializeEntity(this, LinkDoors, INITPRIO_LINKDOORS);
+
+       this.reset = door_rotating_reset;
+}
+#endif
diff --git a/qcsrc/common/mapobjects/func/door_rotating.qh b/qcsrc/common/mapobjects/func/door_rotating.qh
new file mode 100644 (file)
index 0000000..5ff5728
--- /dev/null
@@ -0,0 +1,14 @@
+#pragma once
+
+
+const int DOOR_ROTATING_BIDIR = BIT(1);
+const int DOOR_ROTATING_BIDIR_IN_DOWN = BIT(3);
+
+const int DOOR_ROTATING_XAXIS = BIT(6);
+const int DOOR_ROTATING_YAXIS = BIT(7);
+
+
+#ifdef GAMEQC
+void door_rotating_go_down(entity this);
+void door_rotating_go_up(entity this, entity oth);
+#endif
diff --git a/qcsrc/common/mapobjects/func/door_secret.qc b/qcsrc/common/mapobjects/func/door_secret.qc
new file mode 100644 (file)
index 0000000..78e0dd6
--- /dev/null
@@ -0,0 +1,266 @@
+#include "door_secret.qh"
+#ifdef SVQC
+void fd_secret_move1(entity this);
+void fd_secret_move2(entity this);
+void fd_secret_move3(entity this);
+void fd_secret_move4(entity this);
+void fd_secret_move5(entity this);
+void fd_secret_move6(entity this);
+void fd_secret_done(entity this);
+
+void fd_secret_use(entity this, entity actor, entity trigger)
+{
+       float temp;
+       string message_save;
+
+       this.health = 10000;
+       if(!this.bot_attack)
+               IL_PUSH(g_bot_targets, this);
+       this.bot_attack = true;
+
+       // exit if still moving around...
+       if (this.origin != this.oldorigin)
+               return;
+
+       message_save = this.message;
+       this.message = ""; // no more message
+       SUB_UseTargets(this, actor, trigger);                           // fire all targets / killtargets
+       this.message = message_save;
+
+       this.velocity = '0 0 0';
+
+       // Make a sound, wait a little...
+
+       if (this.noise1 != "")
+               _sound(this, CH_TRIGGER_SINGLE, this.noise1, VOL_BASE, ATTEN_NORM);
+       this.nextthink = this.ltime + 0.1;
+
+       temp = 1 - (this.spawnflags & DOOR_SECRET_1ST_LEFT);    // 1 or -1
+       makevectors(this.mangle);
+
+       if (!this.t_width)
+       {
+               if (this.spawnflags & DOOR_SECRET_1ST_DOWN)
+                       this.t_width = fabs(v_up * this.size);
+               else
+                       this.t_width = fabs(v_right * this.size);
+       }
+
+       if (!this.t_length)
+               this.t_length = fabs(v_forward * this.size);
+
+       if (this.spawnflags & DOOR_SECRET_1ST_DOWN)
+               this.dest1 = this.origin - v_up * this.t_width;
+       else
+               this.dest1 = this.origin + v_right * (this.t_width * temp);
+
+       this.dest2 = this.dest1 + v_forward * this.t_length;
+       SUB_CalcMove(this, this.dest1, TSPEED_LINEAR, this.speed, fd_secret_move1);
+       if (this.noise2 != "")
+               _sound(this, CH_TRIGGER_SINGLE, this.noise2, VOL_BASE, ATTEN_NORM);
+}
+
+void fd_secret_damage(entity this, entity inflictor, entity attacker, float damage, int deathtype, .entity weaponentity, vector hitloc, vector force)
+{
+       fd_secret_use(this, NULL, NULL);
+}
+
+// Wait after first movement...
+void fd_secret_move1(entity this)
+{
+       this.nextthink = this.ltime + 1.0;
+       setthink(this, fd_secret_move2);
+       if (this.noise3 != "")
+               _sound(this, CH_TRIGGER_SINGLE, this.noise3, VOL_BASE, ATTEN_NORM);
+}
+
+// Start moving sideways w/sound...
+void fd_secret_move2(entity this)
+{
+       if (this.noise2 != "")
+               _sound(this, CH_TRIGGER_SINGLE, this.noise2, VOL_BASE, ATTEN_NORM);
+       SUB_CalcMove(this, this.dest2, TSPEED_LINEAR, this.speed, fd_secret_move3);
+}
+
+// Wait here until time to go back...
+void fd_secret_move3(entity this)
+{
+       if (this.noise3 != "")
+               _sound(this, CH_TRIGGER_SINGLE, this.noise3, VOL_BASE, ATTEN_NORM);
+       if (!(this.spawnflags & DOOR_SECRET_OPEN_ONCE) && this.wait >= 0)
+       {
+               this.nextthink = this.ltime + this.wait;
+               setthink(this, fd_secret_move4);
+       }
+}
+
+// Move backward...
+void fd_secret_move4(entity this)
+{
+       if (this.noise2 != "")
+               _sound(this, CH_TRIGGER_SINGLE, this.noise2, VOL_BASE, ATTEN_NORM);
+       SUB_CalcMove(this, this.dest1, TSPEED_LINEAR, this.speed, fd_secret_move5);
+}
+
+// Wait 1 second...
+void fd_secret_move5(entity this)
+{
+       this.nextthink = this.ltime + 1.0;
+       setthink(this, fd_secret_move6);
+       if (this.noise3 != "")
+               _sound(this, CH_TRIGGER_SINGLE, this.noise3, VOL_BASE, ATTEN_NORM);
+}
+
+void fd_secret_move6(entity this)
+{
+       if (this.noise2 != "")
+               _sound(this, CH_TRIGGER_SINGLE, this.noise2, VOL_BASE, ATTEN_NORM);
+       SUB_CalcMove(this, this.oldorigin, TSPEED_LINEAR, this.speed, fd_secret_done);
+}
+
+void fd_secret_done(entity this)
+{
+       if (this.spawnflags&DOOR_SECRET_YES_SHOOT)
+       {
+               this.health = 10000;
+               this.takedamage = DAMAGE_YES;
+               //this.th_pain = fd_secret_use;
+       }
+       if (this.noise3 != "")
+               _sound(this, CH_TRIGGER_SINGLE, this.noise3, VOL_BASE, ATTEN_NORM);
+}
+
+.float door_finished;
+
+void secret_blocked(entity this, entity blocker)
+{
+       if (time < this.door_finished)
+               return;
+       this.door_finished = time + 0.5;
+       //T_Damage (other, this, this, this.dmg, this.dmg, this.deathtype, DT_IMPACT, (this.absmin + this.absmax) * 0.5, '0 0 0', Obituary_Generic);
+}
+
+/*
+==============
+secret_touch
+
+Prints messages
+================
+*/
+void secret_touch(entity this, entity toucher)
+{
+       if (!toucher.iscreature)
+               return;
+       if (this.door_finished > time)
+               return;
+
+       this.door_finished = time + 2;
+
+       if (this.message)
+       {
+               if (IS_CLIENT(toucher))
+                       centerprint(toucher, this.message);
+               play2(toucher, this.noise);
+       }
+}
+
+void secret_reset(entity this)
+{
+       if (this.spawnflags & DOOR_SECRET_YES_SHOOT)
+       {
+               this.health = 10000;
+               this.takedamage = DAMAGE_YES;
+       }
+       setorigin(this, this.oldorigin);
+       setthink(this, func_null);
+       this.nextthink = 0;
+}
+
+/*QUAKED spawnfunc_func_door_secret (0 .5 .8) ? open_once 1st_left 1st_down no_shoot always_shoot
+Basic secret door. Slides back, then to the side. Angle determines direction.
+wait  = # of seconds before coming back
+1st_left = 1st move is left of arrow
+1st_down = 1st move is down from arrow
+always_shoot = even if targeted, keep shootable
+t_width = override WIDTH to move back (or height if going down)
+t_length = override LENGTH to move sideways
+"dmg"          damage to inflict when blocked (2 default)
+
+If a secret door has a targetname, it will only be opened by it's botton or trigger, not by damage.
+"sounds"
+1) medieval
+2) metal
+3) base
+*/
+
+spawnfunc(func_door_secret)
+{
+       /*if (!this.deathtype) // map makers can override this
+               this.deathtype = " got in the way";*/
+
+       if (!this.dmg)
+       {
+               this.dmg = 2;
+       }
+
+       // Magic formula...
+       this.mangle = this.angles;
+       this.angles = '0 0 0';
+       this.classname = "door";
+       if (!InitMovingBrushTrigger(this)) return;
+       this.effects |= EF_LOWPRECISION;
+
+       // TODO: other soundpacks
+       if (this.sounds > 0)
+       {
+               this.noise1 = "plats/medplat1.wav";
+               this.noise2 = "plats/medplat1.wav";
+               this.noise3 = "plats/medplat2.wav";
+       }
+
+       // sound on touch
+       if (this.noise == "")
+       {
+               this.noise = "misc/talk.wav";
+       }
+       precache_sound(this.noise);
+       // sound while moving backwards
+       if (this.noise1 && this.noise1 != "")
+       {
+               precache_sound(this.noise1);
+       }
+       // sound while moving sideways
+       if (this.noise2 && this.noise2 != "")
+       {
+               precache_sound(this.noise2);
+       }
+       // sound when door stops moving
+       if (this.noise3 && this.noise3 != "")
+       {
+               precache_sound(this.noise3);
+       }
+
+       settouch(this, secret_touch);
+       setblocked(this, secret_blocked);
+       this.speed = 50;
+       this.use = fd_secret_use;
+       IFTARGETED
+       {
+       }
+       else
+               this.spawnflags |= DOOR_SECRET_YES_SHOOT;
+
+       if (this.spawnflags & DOOR_SECRET_YES_SHOOT)
+       {
+               //this.canteamdamage = true; // TODO
+               this.health = 10000;
+               this.takedamage = DAMAGE_YES;
+               this.event_damage = fd_secret_damage;
+       }
+       this.oldorigin = this.origin;
+       if (!this.wait) this.wait = 5; // seconds before closing
+
+       this.reset = secret_reset;
+       this.reset(this);
+}
+#endif
diff --git a/qcsrc/common/mapobjects/func/door_secret.qh b/qcsrc/common/mapobjects/func/door_secret.qh
new file mode 100644 (file)
index 0000000..ee575bc
--- /dev/null
@@ -0,0 +1,8 @@
+#pragma once
+
+
+const int DOOR_SECRET_OPEN_ONCE = BIT(0); // stays open - LEGACY, set wait to -1 instead
+const int DOOR_SECRET_1ST_LEFT = BIT(1); // 1st move is left of arrow
+const int DOOR_SECRET_1ST_DOWN = BIT(2); // 1st move is down from arrow
+const int DOOR_SECRET_NO_SHOOT = BIT(3); // only opened by trigger
+const int DOOR_SECRET_YES_SHOOT = BIT(4); // shootable even if targeted
diff --git a/qcsrc/common/mapobjects/func/fourier.qc b/qcsrc/common/mapobjects/func/fourier.qc
new file mode 100644 (file)
index 0000000..28e0f0f
--- /dev/null
@@ -0,0 +1,89 @@
+#include "fourier.qh"
+#ifdef SVQC
+/*QUAKED spawnfunc_func_fourier (0 .5 .8) ?
+Brush model that moves in a pattern of added up sine waves, can be used e.g. for circular motions.
+netname: list of <frequencymultiplier> <phase> <x> <y> <z> quadruples, separated by spaces; note that phase 0 represents a sine wave, and phase 0.25 a cosine wave (by default, it uses 1 0 0 0 1, to match func_bobbing's defaults
+speed: how long one cycle of frequency multiplier 1 in seconds (default 4)
+height: amplitude modifier (default 32)
+phase: cycle timing adjustment (0-1 as a fraction of the cycle, default 0)
+noise: path/name of looping .wav file to play.
+dmg: Do this mutch dmg every .dmgtime intervall when blocked
+dmgtime: See above.
+*/
+
+void func_fourier_controller_think(entity this)
+{
+       vector v;
+       float n, i, t;
+
+       this.nextthink = time + 0.1;
+       if(this.owner.active != ACTIVE_ACTIVE)
+       {
+               this.owner.velocity = '0 0 0';
+               return;
+       }
+
+
+       n = floor((tokenize_console(this.owner.netname)) / 5);
+       t = this.nextthink * this.owner.cnt + this.owner.phase * 360;
+
+       v = this.owner.destvec;
+
+       for(i = 0; i < n; ++i)
+       {
+               makevectors((t * stof(argv(i*5)) + stof(argv(i*5+1)) * 360) * '0 1 0');
+               v = v + ('1 0 0' * stof(argv(i*5+2)) + '0 1 0' * stof(argv(i*5+3)) + '0 0 1' * stof(argv(i*5+4))) * this.owner.height * v_forward_y;
+       }
+
+       if(this.owner.classname == "func_fourier") // don't brake stuff if the func_fourier was killtarget'ed
+               // * 10 so it will arrive in 0.1 sec
+               this.owner.velocity = (v - this.owner.origin) * 10;
+}
+
+spawnfunc(func_fourier)
+{
+       entity controller;
+       if (this.noise != "")
+       {
+               precache_sound(this.noise);
+               soundto(MSG_INIT, this, CH_TRIGGER_SINGLE, this.noise, VOL_BASE, ATTEN_IDLE);
+       }
+
+       if (!this.speed)
+               this.speed = 4;
+       if (!this.height)
+               this.height = 32;
+       this.destvec = this.origin;
+       this.cnt = 360 / this.speed;
+
+       setblocked(this, generic_plat_blocked);
+       if(this.dmg && (this.message == ""))
+               this.message = " was squished";
+    if(this.dmg && (this.message2 == ""))
+               this.message2 = "was squished by";
+       if(this.dmg && (!this.dmgtime))
+               this.dmgtime = 0.25;
+       this.dmgtime2 = time;
+
+       if(this.netname == "")
+               this.netname = "1 0 0 0 1";
+
+       if (!InitMovingBrushTrigger(this))
+               return;
+
+       this.active = ACTIVE_ACTIVE;
+
+       // wait for targets to spawn
+       controller = new(func_fourier_controller);
+       controller.owner = this;
+       controller.nextthink = time + 1;
+       setthink(controller, func_fourier_controller_think);
+       this.nextthink = this.ltime + 999999999;
+       setthink(this, SUB_NullThink); // for PushMove
+
+       // Savage: Reduce bandwith, critical on e.g. nexdm02
+       this.effects |= EF_LOWPRECISION;
+
+       // TODO make a reset function for this one
+}
+#endif
diff --git a/qcsrc/common/mapobjects/func/fourier.qh b/qcsrc/common/mapobjects/func/fourier.qh
new file mode 100644 (file)
index 0000000..6f70f09
--- /dev/null
@@ -0,0 +1 @@
+#pragma once
diff --git a/qcsrc/common/mapobjects/func/include.qc b/qcsrc/common/mapobjects/func/include.qc
new file mode 100644 (file)
index 0000000..290c2e9
--- /dev/null
@@ -0,0 +1,19 @@
+#include "include.qh"
+
+#include "bobbing.qc"
+#include "breakable.qc"
+#include "button.qc"
+#include "conveyor.qc"
+#include "door.qc"
+#include "door_rotating.qc"
+#include "door_secret.qc"
+#include "fourier.qc"
+#include "ladder.qc"
+#include "pendulum.qc"
+#include "plat.qc"
+#include "pointparticles.qc"
+#include "rainsnow.qc"
+#include "rotating.qc"
+#include "stardust.qc"
+#include "train.qc"
+#include "vectormamamam.qc"
diff --git a/qcsrc/common/mapobjects/func/include.qh b/qcsrc/common/mapobjects/func/include.qh
new file mode 100644 (file)
index 0000000..4e8b94c
--- /dev/null
@@ -0,0 +1,5 @@
+#pragma once
+
+#include "door.qh"
+#include "ladder.qh"
+#include "train.qh"
diff --git a/qcsrc/common/mapobjects/func/ladder.qc b/qcsrc/common/mapobjects/func/ladder.qc
new file mode 100644 (file)
index 0000000..020ecca
--- /dev/null
@@ -0,0 +1,148 @@
+#include "ladder.qh"
+REGISTER_NET_LINKED(ENT_CLIENT_LADDER)
+
+void func_ladder_touch(entity this, entity toucher)
+{
+#ifdef SVQC
+       if (!toucher.iscreature)
+               return;
+       if(IS_VEHICLE(toucher))
+               return;
+#elif defined(CSQC)
+       if(!toucher.isplayermodel)
+               return;
+#endif
+
+       EXACTTRIGGER_TOUCH(this, toucher);
+
+       toucher.ladder_time = time + 0.1;
+       toucher.ladder_entity = this;
+}
+
+#ifdef SVQC
+bool func_ladder_send(entity this, entity to, int sf)
+{
+       WriteHeader(MSG_ENTITY, ENT_CLIENT_LADDER);
+
+       WriteString(MSG_ENTITY, this.classname);
+       WriteByte(MSG_ENTITY, this.skin);
+       WriteCoord(MSG_ENTITY, this.speed);
+
+       trigger_common_write(this, false);
+
+       return true;
+}
+
+void func_ladder_link(entity this)
+{
+       trigger_link(this, func_ladder_send);
+       //this.model = "null";
+}
+
+void func_ladder_init(entity this)
+{
+       settouch(this, func_ladder_touch);
+       trigger_init(this);
+       func_ladder_link(this);
+
+       if(min(this.absmax.x - this.absmin.x, this.absmax.y - this.absmin.y) > 100)
+               return;
+
+       entity tracetest_ent = spawn();
+       setsize(tracetest_ent, PL_MIN_CONST, PL_MAX_CONST);
+       tracetest_ent.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_BODY | DPCONTENTS_PLAYERCLIP | DPCONTENTS_BOTCLIP;
+
+       vector top_min = (this.absmin + this.absmax) / 2;
+       top_min.z = this.absmax.z;
+       vector top_max = top_min;
+       top_max.z += PL_MAX_CONST.z - PL_MIN_CONST.z;
+       tracebox(top_max + jumpstepheightvec, PL_MIN_CONST, PL_MAX_CONST, top_min, MOVE_NOMONSTERS, tracetest_ent);
+       if(trace_startsolid)
+       {
+               tracebox(top_max + stepheightvec, PL_MIN_CONST, PL_MAX_CONST, top_min, MOVE_NOMONSTERS, tracetest_ent);
+               if(trace_startsolid)
+               {
+                       tracebox(top_max, PL_MIN_CONST, PL_MAX_CONST, top_min, MOVE_NOMONSTERS, tracetest_ent);
+                       if(trace_startsolid)
+                       {
+                               if(this.absmax.x - this.absmin.x > PL_MAX_CONST.x - PL_MIN_CONST.x
+                                       && this.absmax.y - this.absmin.y < this.absmax.x - this.absmin.x)
+                               {
+                                       // move top on one side
+                                       top_max.y = top_min.y = this.absmin.y + (PL_MAX_CONST.y - PL_MIN_CONST.y) * 0.75;
+                               }
+                               else if(this.absmax.y - this.absmin.y > PL_MAX_CONST.y - PL_MIN_CONST.y
+                                       && this.absmax.x - this.absmin.x < this.absmax.y - this.absmin.y)
+                               {
+                                       // move top on one side
+                                       top_max.x = top_min.x = this.absmin.x + (PL_MAX_CONST.x - PL_MIN_CONST.x) * 0.75;
+                               }
+                               tracebox(top_max, PL_MIN_CONST, PL_MAX_CONST, top_min, MOVE_NOMONSTERS, tracetest_ent);
+                               if(trace_startsolid)
+                               {
+                                       if(this.absmax.x - this.absmin.x > PL_MAX_CONST.x - PL_MIN_CONST.x
+                                               && this.absmax.y - this.absmin.y < this.absmax.x - this.absmin.x)
+                                       {
+                                               // alternatively on the other side
+                                               top_max.y = top_min.y = this.absmax.y - (PL_MAX_CONST.y - PL_MIN_CONST.y) * 0.75;
+                                       }
+                                       else if(this.absmax.y - this.absmin.y > PL_MAX_CONST.y - PL_MIN_CONST.y
+                                               && this.absmax.x - this.absmin.x < this.absmax.y - this.absmin.y)
+                                       {
+                                               // alternatively on the other side
+                                               top_max.x = top_min.x = this.absmax.x - (PL_MAX_CONST.x - PL_MIN_CONST.x) * 0.75;
+                                       }
+                                       tracebox(top_max, PL_MIN_CONST, PL_MAX_CONST, top_min, MOVE_NOMONSTERS, tracetest_ent);
+                               }
+                       }
+               }
+       }
+       if(trace_startsolid || trace_endpos.z < this.absmax.z)
+       {
+               delete(tracetest_ent);
+               return;
+       }
+
+       this.bot_pickup = true; // allow bots to make use of this ladder
+       float cost = waypoint_getlinearcost(trace_endpos.z - this.absmin.z);
+       top_min = trace_endpos;
+       waypoint_spawnforteleporter_boxes(this, WAYPOINTFLAG_LADDER, this.absmin, this.absmax, top_min, top_min, cost);
+}
+
+spawnfunc(func_ladder)
+{
+       IL_PUSH(g_ladders, this); // TODO: also func_water? bots currently loop through func_ladder only
+
+       func_ladder_init(this);
+}
+
+spawnfunc(func_water)
+{
+       func_ladder_init(this);
+}
+
+#elif defined(CSQC)
+.float speed;
+
+void func_ladder_remove(entity this)
+{
+       strfree(this.classname);
+}
+
+NET_HANDLE(ENT_CLIENT_LADDER, bool isnew)
+{
+       this.classname = strzone(ReadString());
+       this.skin = ReadByte();
+       this.speed = ReadCoord();
+
+       trigger_common_read(this, false);
+
+       this.solid = SOLID_TRIGGER;
+       settouch(this, func_ladder_touch);
+       this.drawmask = MASK_NORMAL;
+       this.move_time = time;
+       this.entremove = func_ladder_remove;
+
+       return true;
+}
+#endif
diff --git a/qcsrc/common/mapobjects/func/ladder.qh b/qcsrc/common/mapobjects/func/ladder.qh
new file mode 100644 (file)
index 0000000..26cbbda
--- /dev/null
@@ -0,0 +1,4 @@
+#pragma once
+
+.float ladder_time;
+.entity ladder_entity;
diff --git a/qcsrc/common/mapobjects/func/pendulum.qc b/qcsrc/common/mapobjects/func/pendulum.qc
new file mode 100644 (file)
index 0000000..a59f7a9
--- /dev/null
@@ -0,0 +1,77 @@
+#include "pendulum.qh"
+#ifdef SVQC
+.float freq;
+void func_pendulum_controller_think(entity this)
+{
+       float v;
+       this.nextthink = time + 0.1;
+
+       if (!(this.owner.active == ACTIVE_ACTIVE))
+       {
+               this.owner.avelocity_x = 0;
+               return;
+       }
+
+       // calculate sinewave using makevectors
+       makevectors((this.nextthink * this.owner.freq + this.owner.phase) * '0 360 0');
+       v = this.owner.speed * v_forward_y + this.cnt;
+       if(this.owner.classname == "func_pendulum") // don't brake stuff if the func_bobbing was killtarget'ed
+       {
+               // * 10 so it will arrive in 0.1 sec
+               this.owner.avelocity_z = (remainder(v - this.owner.angles_z, 360)) * 10;
+       }
+}
+
+spawnfunc(func_pendulum)
+{
+       entity controller;
+       if (this.noise != "")
+       {
+               precache_sound(this.noise);
+               soundto(MSG_INIT, this, CH_TRIGGER_SINGLE, this.noise, VOL_BASE, ATTEN_IDLE);
+       }
+
+       this.active = ACTIVE_ACTIVE;
+
+       // keys: angle, speed, phase, noise, freq
+
+       if(!this.speed)
+               this.speed = 30;
+       // not initializing this.dmg to 2, to allow damageless pendulum
+
+       if(this.dmg && (this.message == ""))
+               this.message = " was squished";
+       if(this.dmg && (this.message2 == ""))
+               this.message2 = "was squished by";
+       if(this.dmg && (!this.dmgtime))
+               this.dmgtime = 0.25;
+       this.dmgtime2 = time;
+
+       setblocked(this, generic_plat_blocked);
+
+       this.avelocity_z = 0.0000001;
+       if (!InitMovingBrushTrigger(this))
+               return;
+
+       if(!this.freq)
+       {
+               // find pendulum length (same formula as Q3A)
+               this.freq = 1 / (M_PI * 2) * sqrt(autocvar_sv_gravity / (3 * max(8, fabs(this.mins_z))));
+       }
+
+       // copy initial angle
+       this.cnt = this.angles_z;
+
+       // wait for targets to spawn
+       controller = new(func_pendulum_controller);
+       controller.owner = this;
+       controller.nextthink = time + 1;
+       setthink(controller, func_pendulum_controller_think);
+       this.nextthink = this.ltime + 999999999;
+       setthink(this, SUB_NullThink); // for PushMove
+
+       //this.effects |= EF_LOWPRECISION;
+
+       // TODO make a reset function for this one
+}
+#endif
diff --git a/qcsrc/common/mapobjects/func/pendulum.qh b/qcsrc/common/mapobjects/func/pendulum.qh
new file mode 100644 (file)
index 0000000..6f70f09
--- /dev/null
@@ -0,0 +1 @@
+#pragma once
diff --git a/qcsrc/common/mapobjects/func/plat.qc b/qcsrc/common/mapobjects/func/plat.qc
new file mode 100644 (file)
index 0000000..b052336
--- /dev/null
@@ -0,0 +1,190 @@
+#include "plat.qh"
+REGISTER_NET_LINKED(ENT_CLIENT_PLAT)
+
+#ifdef SVQC
+void plat_link(entity this);
+
+void plat_delayedinit(entity this)
+{
+       plat_link(this);
+       plat_spawn_inside_trigger(this); // the "start moving" trigger
+}
+
+float plat_send(entity this, entity to, float sf)
+{
+       WriteHeader(MSG_ENTITY, ENT_CLIENT_PLAT);
+       WriteByte(MSG_ENTITY, sf);
+
+       if(sf & SF_TRIGGER_INIT)
+       {
+               WriteByte(MSG_ENTITY, this.platmovetype_start);
+               WriteByte(MSG_ENTITY, this.platmovetype_turn);
+               WriteByte(MSG_ENTITY, this.platmovetype_end);
+               WriteByte(MSG_ENTITY, this.spawnflags);
+
+               WriteString(MSG_ENTITY, this.model);
+
+               trigger_common_write(this, true);
+
+               WriteVector(MSG_ENTITY, this.pos1);
+               WriteVector(MSG_ENTITY, this.pos2);
+
+               WriteVector(MSG_ENTITY, this.size);
+
+               WriteAngle(MSG_ENTITY, this.mangle_x);
+               WriteAngle(MSG_ENTITY, this.mangle_y);
+               WriteAngle(MSG_ENTITY, this.mangle_z);
+
+               WriteShort(MSG_ENTITY, this.speed);
+               WriteShort(MSG_ENTITY, this.height);
+               WriteByte(MSG_ENTITY, this.lip);
+               WriteByte(MSG_ENTITY, this.state);
+
+               WriteShort(MSG_ENTITY, this.dmg);
+       }
+
+       if(sf & SF_TRIGGER_RESET)
+       {
+               // used on client
+       }
+
+       return true;
+}
+
+void plat_link(entity this)
+{
+       //Net_LinkEntity(this, 0, false, plat_send);
+}
+
+spawnfunc(func_plat)
+{
+       if (this.spawnflags & CRUSH)
+       {
+               this.dmg = 10000;
+       }
+
+    if (this.dmg && (this.message == ""))
+       {
+               this.message = "was squished";
+       }
+    if (this.dmg && (this.message2 == ""))
+       {
+               this.message2 = "was squished by";
+       }
+
+       if (this.sounds == 1)
+       {
+               this.noise = "plats/plat1.wav";
+               this.noise1 = "plats/plat2.wav";
+       }
+
+       if (this.sounds == 2)
+       {
+               this.noise = "plats/medplat1.wav";
+               this.noise1 = "plats/medplat2.wav";
+       }
+
+       // WARNING: backwards compatibility because people don't use already existing fields :(
+       if (this.sound1)
+               this.noise = this.sound1;
+       if (this.sound2)
+               this.noise1 = this.sound2;
+
+       if(this.noise && this.noise != "")
+       {
+               precache_sound(this.noise);
+       }
+       if(this.noise1 && this.noise1 != "")
+       {
+               precache_sound(this.noise1);
+       }
+
+       this.mangle = this.angles;
+       this.angles = '0 0 0';
+
+       this.classname = "plat";
+       if (!InitMovingBrushTrigger(this))
+               return;
+       this.effects |= EF_LOWPRECISION;
+       setsize (this, this.mins , this.maxs);
+
+       setblocked(this, plat_crush);
+
+       if (!this.speed) this.speed = 150;
+       if (!this.lip) this.lip = 16;
+       if (!this.height) this.height = this.size.z - this.lip;
+
+       this.pos1 = this.origin;
+       this.pos2 = this.origin;
+       this.pos2_z = this.origin.z - this.height;
+
+       this.reset = plat_reset;
+       this.reset(this);
+
+       InitializeEntity(this, plat_delayedinit, INITPRIO_FINDTARGET);
+}
+#elif defined(CSQC)
+void plat_draw(entity this)
+{
+       Movetype_Physics_NoMatchServer(this);
+       //Movetype_Physics_MatchServer(autocvar_cl_projectiles_sloppy);
+}
+
+NET_HANDLE(ENT_CLIENT_PLAT, bool isnew)
+{
+       float sf = ReadByte();
+
+       if(sf & SF_TRIGGER_INIT)
+       {
+               this.platmovetype_start = ReadByte();
+               this.platmovetype_turn = ReadByte();
+               this.platmovetype_end = ReadByte();
+               this.spawnflags = ReadByte();
+
+               this.model = strzone(ReadString());
+               _setmodel(this, this.model);
+
+               trigger_common_read(this, true);
+
+               this.pos1 = ReadVector();
+               this.pos2 = ReadVector();
+
+               this.size = ReadVector();
+
+               this.mangle_x = ReadAngle();
+               this.mangle_y = ReadAngle();
+               this.mangle_z = ReadAngle();
+
+               this.speed = ReadShort();
+               this.height = ReadShort();
+               this.lip = ReadByte();
+               this.state = ReadByte();
+
+               this.dmg = ReadShort();
+
+               this.classname = "plat";
+               this.solid = SOLID_BSP;
+               set_movetype(this, MOVETYPE_PUSH);
+               this.drawmask = MASK_NORMAL;
+               this.draw = plat_draw;
+               if (isnew) IL_PUSH(g_drawables, this);
+               this.use = plat_use;
+               this.entremove = trigger_remove_generic;
+
+               plat_reset(this); // also called here
+
+               set_movetype(this, MOVETYPE_PUSH);
+               this.move_time = time;
+
+               plat_spawn_inside_trigger(this);
+       }
+
+       if(sf & SF_TRIGGER_RESET)
+       {
+               plat_reset(this);
+
+               this.move_time = time;
+       }
+       return true;
+}
+#endif
diff --git a/qcsrc/common/mapobjects/func/plat.qh b/qcsrc/common/mapobjects/func/plat.qh
new file mode 100644 (file)
index 0000000..6f70f09
--- /dev/null
@@ -0,0 +1 @@
+#pragma once
diff --git a/qcsrc/common/mapobjects/func/pointparticles.qc b/qcsrc/common/mapobjects/func/pointparticles.qc
new file mode 100644 (file)
index 0000000..7de5a03
--- /dev/null
@@ -0,0 +1,341 @@
+#include "pointparticles.qh"
+REGISTER_NET_LINKED(ENT_CLIENT_POINTPARTICLES)
+
+#ifdef SVQC
+// NOTE: also contains func_sparks
+
+bool pointparticles_SendEntity(entity this, entity to, float sendflags)
+{
+       WriteHeader(MSG_ENTITY, ENT_CLIENT_POINTPARTICLES);
+
+       // optional features to save space
+       sendflags = sendflags & 0x0F;
+       if(this.spawnflags & PARTICLES_IMPULSE)
+               sendflags |= SF_POINTPARTICLES_IMPULSE; // absolute count on toggle-on
+       if(this.movedir != '0 0 0' || this.velocity != '0 0 0')
+               sendflags |= SF_POINTPARTICLES_MOVING; // 4 bytes - saves CPU
+       if(this.waterlevel || this.count != 1)
+               sendflags |= SF_POINTPARTICLES_JITTER_AND_COUNT; // 4 bytes - obscure features almost never used
+       if(this.mins != '0 0 0' || this.maxs != '0 0 0')
+               sendflags |= SF_POINTPARTICLES_BOUNDS; // 14 bytes - saves lots of space
+
+       WriteByte(MSG_ENTITY, sendflags);
+       if(sendflags & SF_TRIGGER_UPDATE)
+       {
+               if(this.active == ACTIVE_ACTIVE)
+                       WriteCoord(MSG_ENTITY, this.impulse);
+               else
+                       WriteCoord(MSG_ENTITY, 0); // off
+       }
+       if(sendflags & SF_TRIGGER_RESET)
+       {
+               WriteVector(MSG_ENTITY, this.origin);
+       }
+       if(sendflags & SF_TRIGGER_INIT)
+       {
+               if(this.model != "null")
+               {
+                       WriteShort(MSG_ENTITY, this.modelindex);
+                       if(sendflags & SF_POINTPARTICLES_BOUNDS)
+                       {
+                               WriteVector(MSG_ENTITY, this.mins);
+                               WriteVector(MSG_ENTITY, this.maxs);
+                       }
+               }
+               else
+               {
+                       WriteShort(MSG_ENTITY, 0);
+                       if(sendflags & SF_POINTPARTICLES_BOUNDS)
+                       {
+                               WriteVector(MSG_ENTITY, this.maxs);
+                       }
+               }
+               WriteShort(MSG_ENTITY, this.cnt);
+               WriteString(MSG_ENTITY, this.mdl);
+               if(sendflags & SF_POINTPARTICLES_MOVING)
+               {
+                       WriteShort(MSG_ENTITY, compressShortVector(this.velocity));
+                       WriteShort(MSG_ENTITY, compressShortVector(this.movedir));
+               }
+               if(sendflags & SF_POINTPARTICLES_JITTER_AND_COUNT)
+               {
+                       WriteShort(MSG_ENTITY, this.waterlevel * 16.0);
+                       WriteByte(MSG_ENTITY, this.count * 16.0);
+               }
+               WriteString(MSG_ENTITY, this.noise);
+               if(this.noise != "")
+               {
+                       WriteByte(MSG_ENTITY, floor(this.atten * 64));
+                       WriteByte(MSG_ENTITY, floor(this.volume * 255));
+               }
+               WriteString(MSG_ENTITY, this.bgmscript);
+               if(this.bgmscript != "")
+               {
+                       WriteByte(MSG_ENTITY, floor(this.bgmscriptattack * 64));
+                       WriteByte(MSG_ENTITY, floor(this.bgmscriptdecay * 64));
+                       WriteByte(MSG_ENTITY, floor(this.bgmscriptsustain * 255));
+                       WriteByte(MSG_ENTITY, floor(this.bgmscriptrelease * 64));
+               }
+       }
+       return 1;
+}
+
+void pointparticles_think(entity this)
+{
+       if(this.origin != this.oldorigin)
+       {
+               this.SendFlags |= SF_TRIGGER_RESET;
+               this.oldorigin = this.origin;
+       }
+       this.nextthink = time;
+}
+
+spawnfunc(func_pointparticles)
+{
+       if(this.model != "") { precache_model(this.model); _setmodel(this, this.model); }
+       if(this.noise != "") precache_sound(this.noise);
+       if(this.mdl != "") this.cnt = 0; // use a good handler
+
+       if(!this.bgmscriptsustain) this.bgmscriptsustain = 1;
+       else if(this.bgmscriptsustain < 0) this.bgmscriptsustain = 0;
+
+       if(!this.atten) this.atten = ATTEN_NORM;
+       else if(this.atten < 0) this.atten = 0;
+       if(!this.volume) this.volume = 1;
+       if(!this.count) this.count = 1;
+       if(!this.impulse) this.impulse = 1;
+
+       if(!this.modelindex)
+       {
+               setorigin(this, this.origin + this.mins);
+               setsize(this, '0 0 0', this.maxs - this.mins);
+       }
+       //if(!this.cnt) this.cnt = _particleeffectnum(this.mdl);
+       this.setactive = generic_netlinked_setactive;
+
+       Net_LinkEntity(this, (this.spawnflags & PARTICLES_VISCULLING), 0, pointparticles_SendEntity);
+
+       IFTARGETED
+       {
+               // backwards compatibility
+               this.use = generic_netlinked_legacy_use;
+       }
+       this.reset = generic_netlinked_reset;
+       this.reset(this);
+       setthink(this, pointparticles_think);
+       this.nextthink = time;
+}
+
+spawnfunc(func_sparks)
+{
+       if(this.count < 1) {
+               this.count = 25.0; // nice default value
+       }
+
+       if(this.impulse < 0.5) {
+               this.impulse = 2.5; // nice default value
+       }
+
+       this.mins = '0 0 0';
+       this.maxs = '0 0 0';
+       this.velocity = '0 0 -1';
+       this.mdl = "TE_SPARK";
+       this.cnt = 0; // use mdl
+
+       spawnfunc_func_pointparticles(this);
+}
+#elif defined(CSQC)
+
+.int dphitcontentsmask;
+
+entityclass(PointParticles);
+classfield(PointParticles) .int cnt; // effect number
+classfield(PointParticles) .vector velocity; // particle velocity
+classfield(PointParticles) .float waterlevel; // direction jitter
+classfield(PointParticles) .int count; // count multiplier
+classfield(PointParticles) .int impulse; // density
+classfield(PointParticles) .string noise; // sound
+classfield(PointParticles) .float atten;
+classfield(PointParticles) .float volume;
+classfield(PointParticles) .float absolute; // 1 = count per second is absolute, ABSOLUTE_ONLY_SPAWN_AT_TOGGLE = only spawn at toggle
+classfield(PointParticles) .vector movedir; // trace direction
+classfield(PointParticles) .float glow_color; // palette index
+
+const int ABSOLUTE_ONLY_SPAWN_AT_TOGGLE = 2;
+
+void Draw_PointParticles(entity this)
+{
+       float n, i, fail;
+       vector p;
+       vector sz;
+       vector o;
+       o = this.origin;
+       sz = this.maxs - this.mins;
+       n = doBGMScript(this);
+       if(this.absolute == ABSOLUTE_ONLY_SPAWN_AT_TOGGLE)
+       {
+               if(n >= 0)
+                       n = this.just_toggled ? this.impulse : 0;
+               else
+                       n = this.impulse * drawframetime;
+       }
+       else
+       {
+               n *= this.impulse * drawframetime;
+               if(this.just_toggled)
+                       if(n < 1)
+                               n = 1;
+       }
+       if(n == 0)
+               return;
+       fail = 0;
+       for(i = random(); i <= n && fail <= 64*n; ++i)
+       {
+               p = o + this.mins;
+               p.x += random() * sz.x;
+               p.y += random() * sz.y;
+               p.z += random() * sz.z;
+               if(WarpZoneLib_BoxTouchesBrush(p, p, this, NULL))
+               {
+                       if(this.movedir != '0 0 0')
+                       {
+                               traceline(p, p + normalize(this.movedir) * 4096, 0, NULL);
+                               p = trace_endpos;
+                               int eff_num;
+                               if(this.cnt)
+                                       eff_num = this.cnt;
+                               else
+                                       eff_num = _particleeffectnum(this.mdl);
+                               __pointparticles(eff_num, p, trace_plane_normal * vlen(this.movedir) + this.velocity + randomvec() * this.waterlevel, this.count);
+                       }
+                       else
+                       {
+                               int eff_num;
+                               if(this.cnt)
+                                       eff_num = this.cnt;
+                               else
+                                       eff_num = _particleeffectnum(this.mdl);
+                               __pointparticles(eff_num, p, this.velocity + randomvec() * this.waterlevel, this.count);
+                       }
+                       if(this.noise != "")
+                       {
+                               setorigin(this, p);
+                               _sound(this, CH_AMBIENT, this.noise, VOL_BASE * this.volume, this.atten);
+                       }
+                       this.just_toggled = 0;
+               }
+               else if(this.absolute)
+               {
+                       ++fail;
+                       --i;
+               }
+       }
+       setorigin(this, o);
+}
+
+void Ent_PointParticles_Remove(entity this)
+{
+    strfree(this.noise);
+    strfree(this.bgmscript);
+    strfree(this.mdl);
+}
+
+NET_HANDLE(ENT_CLIENT_POINTPARTICLES, bool isnew)
+{
+       float i;
+       vector v;
+       int sendflags = ReadByte();
+       if(sendflags & SF_TRIGGER_UPDATE)
+       {
+               i = ReadCoord(); // density (<0: point, >0: volume)
+               if(i && !this.impulse && (this.cnt || this.mdl)) // this.cnt check is so it only happens if the ent already existed
+                       this.just_toggled = 1;
+               this.impulse = i;
+       }
+       if(sendflags & SF_TRIGGER_RESET)
+       {
+               this.origin = ReadVector();
+       }
+       if(sendflags & SF_TRIGGER_INIT)
+       {
+               this.modelindex = ReadShort();
+               if(sendflags & SF_POINTPARTICLES_BOUNDS)
+               {
+                       if(this.modelindex)
+                       {
+                               this.mins = ReadVector();
+                               this.maxs = ReadVector();
+                       }
+                       else
+                       {
+                               this.mins    = '0 0 0';
+                               this.maxs = ReadVector();
+                       }
+               }
+               else
+               {
+                       this.mins = this.maxs = '0 0 0';
+               }
+
+               this.cnt = ReadShort(); // effect number
+               this.mdl = strzone(ReadString()); // effect string
+
+               if(sendflags & SF_POINTPARTICLES_MOVING)
+               {
+                       this.velocity = decompressShortVector(ReadShort());
+                       this.movedir = decompressShortVector(ReadShort());
+               }
+               else
+               {
+                       this.velocity = this.movedir = '0 0 0';
+               }
+               if(sendflags & SF_POINTPARTICLES_JITTER_AND_COUNT)
+               {
+                       this.waterlevel = ReadShort() / 16.0;
+                       this.count = ReadByte() / 16.0;
+               }
+               else
+               {
+                       this.waterlevel = 0;
+                       this.count = 1;
+               }
+               strcpy(this.noise, ReadString());
+               if(this.noise != "")
+               {
+                       this.atten = ReadByte() / 64.0;
+                       this.volume = ReadByte() / 255.0;
+               }
+               strcpy(this.bgmscript, ReadString());
+               if(this.bgmscript != "")
+               {
+                       this.bgmscriptattack = ReadByte() / 64.0;
+                       this.bgmscriptdecay = ReadByte() / 64.0;
+                       this.bgmscriptsustain = ReadByte() / 255.0;
+                       this.bgmscriptrelease = ReadByte() / 64.0;
+               }
+               BGMScript_InitEntity(this);
+       }
+
+       return = true;
+
+       if(sendflags & SF_TRIGGER_UPDATE)
+       {
+               this.absolute = (this.impulse >= 0);
+               if(!this.absolute)
+               {
+                       v = this.maxs - this.mins;
+                       this.impulse *= -v.x * v.y * v.z / (64**3); // relative: particles per 64^3 cube
+               }
+       }
+
+       if(sendflags & SF_POINTPARTICLES_IMPULSE)
+               this.absolute = ABSOLUTE_ONLY_SPAWN_AT_TOGGLE;
+
+       setorigin(this, this.origin);
+       setsize(this, this.mins, this.maxs);
+       this.solid = SOLID_NOT;
+       this.draw = Draw_PointParticles;
+       if (isnew) IL_PUSH(g_drawables, this);
+       this.entremove = Ent_PointParticles_Remove;
+}
+#endif
diff --git a/qcsrc/common/mapobjects/func/pointparticles.qh b/qcsrc/common/mapobjects/func/pointparticles.qh
new file mode 100644 (file)
index 0000000..b167527
--- /dev/null
@@ -0,0 +1,11 @@
+#pragma once
+
+// spawnflags
+const int PARTICLES_IMPULSE = BIT(1);
+const int PARTICLES_VISCULLING = BIT(2);
+
+// sendflags
+const int SF_POINTPARTICLES_IMPULSE = BIT(4);
+const int SF_POINTPARTICLES_MOVING = BIT(5); // Send velocity and movedir
+const int SF_POINTPARTICLES_JITTER_AND_COUNT = BIT(6); // Send waterlevel (=jitter) and count
+const int SF_POINTPARTICLES_BOUNDS = BIT(7); // Send min and max of the brush
diff --git a/qcsrc/common/mapobjects/func/rainsnow.qc b/qcsrc/common/mapobjects/func/rainsnow.qc
new file mode 100644 (file)
index 0000000..c765a42
--- /dev/null
@@ -0,0 +1,142 @@
+#include "rainsnow.qh"
+REGISTER_NET_LINKED(ENT_CLIENT_RAINSNOW)
+
+#ifdef SVQC
+bool rainsnow_SendEntity(entity this, entity to, float sf)
+{
+       vector myorg = this.origin + this.mins;
+       vector mysize = this.maxs - this.mins;
+       WriteHeader(MSG_ENTITY, ENT_CLIENT_RAINSNOW);
+       WriteByte(MSG_ENTITY, this.state);
+       WriteVector(MSG_ENTITY, myorg);
+       WriteVector(MSG_ENTITY, mysize);
+       WriteShort(MSG_ENTITY, compressShortVector(this.dest));
+       WriteShort(MSG_ENTITY, this.count);
+       WriteByte(MSG_ENTITY, this.cnt);
+       return true;
+}
+
+/*QUAKED spawnfunc_func_rain (0 .5 .8) ?
+This is an invisible area like a trigger, which rain falls inside of.
+
+Keys:
+"velocity"
+ falling direction (should be something like '0 0 -700', use the X and Y velocity for wind)
+"cnt"
+ sets color of rain (default 12 - white)
+"count"
+ adjusts density, this many particles fall every second for a 1024x1024 area, default is 2000
+*/
+spawnfunc(func_rain)
+{
+       this.dest = this.velocity;
+       this.velocity = '0 0 0';
+       if (!this.dest)
+               this.dest = '0 0 -700';
+       this.angles = '0 0 0';
+       set_movetype(this, MOVETYPE_NONE);
+       this.solid = SOLID_NOT;
+       SetBrushEntityModel(this);
+       if (!this.cnt)
+       {
+               this.cnt = 12;
+       }
+       if (!this.count)
+               this.count = 2000;
+       // relative to absolute particle count
+       this.count = 0.1 * this.count * (this.size_x / 1024) * (this.size_y / 1024);
+       if (this.count < 1)
+               this.count = 1;
+       if(this.count > 65535)
+               this.count = 65535;
+
+       this.state = RAINSNOW_RAIN;
+
+       Net_LinkEntity(this, false, 0, rainsnow_SendEntity);
+}
+
+
+/*QUAKED spawnfunc_func_snow (0 .5 .8) ?
+This is an invisible area like a trigger, which snow falls inside of.
+
+Keys:
+"velocity"
+ falling direction (should be something like '0 0 -300', use the X and Y velocity for wind)
+"cnt"
+ sets color of rain (default 12 - white)
+"count"
+ adjusts density, this many particles fall every second for a 1024x1024 area, default is 2000
+*/
+spawnfunc(func_snow)
+{
+       this.dest = this.velocity;
+       this.velocity = '0 0 0';
+       if (!this.dest)
+               this.dest = '0 0 -300';
+       this.angles = '0 0 0';
+       set_movetype(this, MOVETYPE_NONE);
+       this.solid = SOLID_NOT;
+       SetBrushEntityModel(this);
+       if (!this.cnt)
+       {
+               this.cnt = 12;
+       }
+       if (!this.count)
+               this.count = 2000;
+       // relative to absolute particle count
+       this.count = 0.1 * this.count * (this.size_x / 1024) * (this.size_y / 1024);
+       if (this.count < 1)
+               this.count = 1;
+       if(this.count > 65535)
+               this.count = 65535;
+
+       this.state = RAINSNOW_SNOW;
+
+       Net_LinkEntity(this, false, 0, rainsnow_SendEntity);
+}
+#elif defined(CSQC)
+float autocvar_cl_rainsnow_maxdrawdist = 2048;
+
+void Draw_Rain(entity this)
+{
+       vector maxdist = '1 1 0' * autocvar_cl_rainsnow_maxdrawdist;
+       maxdist.z = 5;
+       if(boxesoverlap(vec2(view_origin) - maxdist, vec2(view_origin) + maxdist, vec2(this.absmin) - '0 0 5', vec2(this.absmax) + '0 0 5'))
+       //if(autocvar_cl_rainsnow_maxdrawdist <= 0 || vdist(vec2(this.origin) - vec2(this.absmin + this.absmax * 0.5), <=, autocvar_cl_rainsnow_maxdrawdist))
+       te_particlerain(this.origin + this.mins, this.origin + this.maxs, this.velocity, floor(this.count * drawframetime + random()), this.glow_color);
+}
+
+void Draw_Snow(entity this)
+{
+       vector maxdist = '1 1 0' * autocvar_cl_rainsnow_maxdrawdist;
+       maxdist.z = 5;
+       if(boxesoverlap(vec2(view_origin) - maxdist, vec2(view_origin) + maxdist, vec2(this.absmin) - '0 0 5', vec2(this.absmax) + '0 0 5'))
+       //if(autocvar_cl_rainsnow_maxdrawdist <= 0 || vdist(vec2(this.origin) - vec2(this.absmin + this.absmax * 0.5), <=, autocvar_cl_rainsnow_maxdrawdist))
+       te_particlesnow(this.origin + this.mins, this.origin + this.maxs, this.velocity, floor(this.count * drawframetime + random()), this.glow_color);
+}
+
+NET_HANDLE(ENT_CLIENT_RAINSNOW, bool isnew)
+{
+       this.state = ReadByte(); // Rain, Snow, or Whatever
+       this.origin = ReadVector();
+       this.maxs = ReadVector();
+       this.velocity = decompressShortVector(ReadShort());
+       this.count = ReadShort();
+       this.glow_color = ReadByte(); // color
+
+       return = true;
+
+       this.mins    = -0.5 * this.maxs;
+       this.maxs    =  0.5 * this.maxs;
+       this.origin  = this.origin - this.mins;
+
+       setorigin(this, this.origin);
+       setsize(this, this.mins, this.maxs);
+       this.solid = SOLID_NOT;
+       if (isnew) IL_PUSH(g_drawables, this);
+       if(this.state == RAINSNOW_RAIN)
+               this.draw = Draw_Rain;
+       else
+               this.draw = Draw_Snow;
+}
+#endif
diff --git a/qcsrc/common/mapobjects/func/rainsnow.qh b/qcsrc/common/mapobjects/func/rainsnow.qh
new file mode 100644 (file)
index 0000000..d60eb4f
--- /dev/null
@@ -0,0 +1,5 @@
+#pragma once
+
+
+const int RAINSNOW_SNOW = 0;
+const int RAINSNOW_RAIN = 1;
diff --git a/qcsrc/common/mapobjects/func/rotating.qc b/qcsrc/common/mapobjects/func/rotating.qc
new file mode 100644 (file)
index 0000000..35351ee
--- /dev/null
@@ -0,0 +1,110 @@
+#include "rotating.qh"
+#ifdef SVQC
+
+void func_rotating_setactive(entity this, int astate)
+{
+       if (astate == ACTIVE_TOGGLE)
+       {
+               if(this.active == ACTIVE_ACTIVE)
+                       this.active = ACTIVE_NOT;
+               else
+                       this.active = ACTIVE_ACTIVE;
+       }
+       else
+               this.active = astate;
+
+       if(this.active  == ACTIVE_NOT)
+       {
+               this.avelocity = '0 0 0';
+               stopsound(this, CH_AMBIENT_SINGLE);
+       }
+       else
+       {
+               this.avelocity = this.pos1;
+               if(this.noise && this.noise != "")
+               {
+                       _sound(this, CH_AMBIENT_SINGLE, this.noise, VOL_BASE, ATTEN_IDLE);
+               }
+       }
+}
+
+void func_rotating_reset(entity this)
+{
+       // TODO: reset angles as well?
+
+       if(this.spawnflags & FUNC_ROTATING_STARTOFF)
+       {
+               this.setactive(this, ACTIVE_NOT);
+       }
+       else
+       {
+               this.setactive(this, ACTIVE_ACTIVE);
+       }
+}
+
+void func_rotating_init_for_player(entity this, entity player)
+{
+       if (this.noise && this.noise != "" && this.active == ACTIVE_ACTIVE && IS_REAL_CLIENT(player))
+       {
+               msg_entity = player;
+               soundto (MSG_ONE, this, CH_AMBIENT_SINGLE, this.noise, VOL_BASE, ATTEN_IDLE);
+       }
+}
+
+/*QUAKED spawnfunc_func_rotating (0 .5 .8) ? - - X_AXIS Y_AXIS
+Brush model that spins in place on one axis (default Z).
+speed   : speed to rotate (in degrees per second)
+noise   : path/name of looping .wav file to play.
+dmg     : Do this mutch dmg every .dmgtime intervall when blocked
+dmgtime : See above.
+*/
+
+spawnfunc(func_rotating)
+{
+       if (this.noise && this.noise != "")
+       {
+               precache_sound(this.noise);
+       }
+
+       this.setactive = func_rotating_setactive;
+
+       if (!this.speed)
+               this.speed = 100;
+       if (this.spawnflags & FUNC_ROTATING_XAXIS)
+               this.avelocity = '0 0 1' * this.speed;
+       else if (this.spawnflags & FUNC_ROTATING_YAXIS)
+               this.avelocity = '1 0 0' * this.speed;
+       else // Z
+               this.avelocity = '0 1 0' * this.speed;
+
+       this.pos1 = this.avelocity;
+
+    if(this.dmg && (this.message == ""))
+        this.message = " was squished";
+    if(this.dmg && (this.message2 == ""))
+               this.message2 = "was squished by";
+
+
+    if(this.dmg && (!this.dmgtime))
+        this.dmgtime = 0.25;
+
+    this.dmgtime2 = time;
+
+       if (!InitMovingBrushTrigger(this))
+               return;
+       // no EF_LOWPRECISION here, as rounding angles is bad
+
+    setblocked(this, generic_plat_blocked);
+
+       // wait for targets to spawn
+       this.nextthink = this.ltime + 999999999;
+       setthink(this, SUB_NullThink); // for PushMove
+
+       this.reset = func_rotating_reset;
+       this.reset(this);
+
+       // maybe send sound to new players
+       IL_PUSH(g_initforplayer, this);
+       this.init_for_player = func_rotating_init_for_player;
+}
+#endif
diff --git a/qcsrc/common/mapobjects/func/rotating.qh b/qcsrc/common/mapobjects/func/rotating.qh
new file mode 100644 (file)
index 0000000..ad1b6ec
--- /dev/null
@@ -0,0 +1,6 @@
+#pragma once
+
+
+const int FUNC_ROTATING_XAXIS = BIT(2);
+const int FUNC_ROTATING_YAXIS = BIT(3);
+const int FUNC_ROTATING_STARTOFF = BIT(4);
diff --git a/qcsrc/common/mapobjects/func/stardust.qc b/qcsrc/common/mapobjects/func/stardust.qc
new file mode 100644 (file)
index 0000000..9c2fba8
--- /dev/null
@@ -0,0 +1,9 @@
+#include "stardust.qh"
+#ifdef SVQC
+spawnfunc(func_stardust)
+{
+       this.effects = EF_STARDUST;
+
+       CSQCMODEL_AUTOINIT(this);
+}
+#endif
diff --git a/qcsrc/common/mapobjects/func/stardust.qh b/qcsrc/common/mapobjects/func/stardust.qh
new file mode 100644 (file)
index 0000000..6f70f09
--- /dev/null
@@ -0,0 +1 @@
+#pragma once
diff --git a/qcsrc/common/mapobjects/func/train.qc b/qcsrc/common/mapobjects/func/train.qc
new file mode 100644 (file)
index 0000000..4e9c334
--- /dev/null
@@ -0,0 +1,351 @@
+#include "train.qh"
+.float train_wait_turning;
+.entity future_target;
+void train_next(entity this);
+#ifdef SVQC
+void train_use(entity this, entity actor, entity trigger);
+#endif
+void train_wait(entity this)
+{
+       SUB_UseTargets(this.enemy, NULL, NULL);
+       this.enemy = NULL;
+
+       // if turning is enabled, the train will turn toward the next point while waiting
+       if(this.platmovetype_turn && !this.train_wait_turning)
+       {
+               entity targ, cp;
+               vector ang;
+               targ = this.future_target;
+               if((this.spawnflags & TRAIN_CURVE) && targ.curvetarget)
+                       cp = find(NULL, targetname, targ.curvetarget);
+               else
+                       cp = NULL;
+
+               if(cp) // bezier curves movement
+                       ang = cp.origin - (this.origin - this.view_ofs); // use the origin of the control point of the next path_corner
+               else // linear movement
+                       ang = targ.origin - (this.origin - this.view_ofs); // use the origin of the next path_corner
+               ang = vectoangles(ang);
+               ang_x = -ang_x; // flip up / down orientation
+
+               if(this.wait > 0) // slow turning
+                       SUB_CalcAngleMove(this, ang, TSPEED_TIME, this.ltime - time + this.wait, train_wait);
+               else // instant turning
+                       SUB_CalcAngleMove(this, ang, TSPEED_TIME, 0.0000001, train_wait);
+               this.train_wait_turning = true;
+               return;
+       }
+
+#ifdef SVQC
+       if(this.noise != "")
+               stopsoundto(MSG_BROADCAST, this, CH_TRIGGER_SINGLE); // send this as unreliable only, as the train will resume operation shortly anyway
+#endif
+
+#ifdef SVQC
+       entity tg = this.future_target;
+       if(tg.spawnflags & TRAIN_NEEDACTIVATION)
+       {
+               this.use = train_use;
+               setthink(this, func_null);
+               this.nextthink = 0;
+       }
+       else
+#endif
+            if(this.wait < 0 || this.train_wait_turning) // no waiting or we already waited while turning
+       {
+               this.train_wait_turning = false;
+               train_next(this);
+       }
+       else
+       {
+               setthink(this, train_next);
+               this.nextthink = this.ltime + this.wait;
+       }
+}
+
+entity train_next_find(entity this)
+{
+       if(this.target_random)
+       {
+               RandomSelection_Init();
+               for(entity t = NULL; (t = find(t, targetname, this.target));)
+               {
+                       RandomSelection_AddEnt(t, 1, 0);
+               }
+               return RandomSelection_chosen_ent;
+       }
+       else
+       {
+               return find(NULL, targetname, this.target);
+       }
+}
+
+void train_next(entity this)
+{
+       entity targ = NULL, cp = NULL;
+       vector cp_org = '0 0 0';
+
+       targ = this.future_target;
+
+       this.target = targ.target;
+       this.target_random = targ.target_random;
+       this.future_target = train_next_find(targ);
+
+       if (this.spawnflags & TRAIN_CURVE)
+       {
+               if(targ.curvetarget)
+               {
+                       cp = find(NULL, targetname, targ.curvetarget); // get its second target (the control point)
+                       cp_org = cp.origin - this.view_ofs; // no control point found, assume a straight line to the destination
+               }
+       }
+       if (this.target == "")
+               objerror(this, "train_next: no next target");
+       this.wait = targ.wait;
+       if (!this.wait)
+               this.wait = 0.1;
+
+       if(targ.platmovetype)
+       {
+               // this path_corner contains a movetype overrider, apply it
+               this.platmovetype_start = targ.platmovetype_start;
+               this.platmovetype_end = targ.platmovetype_end;
+       }
+       else
+       {
+               // this path_corner doesn't contain a movetype overrider, use the train's defaults
+               this.platmovetype_start = this.platmovetype_start_default;
+               this.platmovetype_end = this.platmovetype_end_default;
+       }
+
+       if (targ.speed)
+       {
+               if (cp)
+                       SUB_CalcMove_Bezier(this, cp_org, targ.origin - this.view_ofs, TSPEED_LINEAR, targ.speed, train_wait);
+               else
+                       SUB_CalcMove(this, targ.origin - this.view_ofs, TSPEED_LINEAR, targ.speed, train_wait);
+       }
+       else
+       {
+               if (cp)
+                       SUB_CalcMove_Bezier(this, cp_org, targ.origin - this.view_ofs, TSPEED_LINEAR, this.speed, train_wait);
+               else
+                       SUB_CalcMove(this, targ.origin - this.view_ofs, TSPEED_LINEAR, this.speed, train_wait);
+       }
+
+       if(this.noise != "")
+               _sound(this, CH_TRIGGER_SINGLE, this.noise, VOL_BASE, ATTEN_IDLE);
+}
+
+REGISTER_NET_LINKED(ENT_CLIENT_TRAIN)
+
+#ifdef SVQC
+float train_send(entity this, entity to, float sf)
+{
+       WriteHeader(MSG_ENTITY, ENT_CLIENT_TRAIN);
+       WriteByte(MSG_ENTITY, sf);
+
+       if(sf & SF_TRIGGER_INIT)
+       {
+               WriteString(MSG_ENTITY, this.platmovetype);
+               WriteByte(MSG_ENTITY, this.platmovetype_turn);
+               WriteByte(MSG_ENTITY, this.spawnflags);
+
+               WriteString(MSG_ENTITY, this.model);
+
+               trigger_common_write(this, true);
+
+               WriteString(MSG_ENTITY, this.curvetarget);
+
+               WriteVector(MSG_ENTITY, this.pos1);
+               WriteVector(MSG_ENTITY, this.pos2);
+
+               WriteVector(MSG_ENTITY, this.size);
+
+               WriteVector(MSG_ENTITY, this.view_ofs);
+
+               WriteAngle(MSG_ENTITY, this.mangle_x);
+               WriteAngle(MSG_ENTITY, this.mangle_y);
+               WriteAngle(MSG_ENTITY, this.mangle_z);
+
+               WriteShort(MSG_ENTITY, this.speed);
+               WriteShort(MSG_ENTITY, this.height);
+               WriteByte(MSG_ENTITY, this.lip);
+               WriteByte(MSG_ENTITY, this.state);
+               WriteByte(MSG_ENTITY, this.wait);
+
+               WriteShort(MSG_ENTITY, this.dmg);
+               WriteByte(MSG_ENTITY, this.dmgtime);
+       }
+
+       if(sf & SF_TRIGGER_RESET)
+       {
+               // used on client
+       }
+
+       return true;
+}
+
+void train_link(entity this)
+{
+       //Net_LinkEntity(this, 0, false, train_send);
+}
+
+void train_use(entity this, entity actor, entity trigger)
+{
+       this.nextthink = this.ltime + 1;
+       setthink(this, train_next);
+       this.use = func_null; // not again, next target can set it again if needed
+       if(trigger.target2 && trigger.target2 != "")
+               this.future_target = find(NULL, targetname, trigger.target2);
+}
+
+void func_train_find(entity this)
+{
+       entity targ = train_next_find(this);
+       this.target = targ.target;
+       this.target_random = targ.target_random;
+       // save the future target for later
+       this.future_target = train_next_find(targ);
+       if (this.target == "")
+               objerror(this, "func_train_find: no next target");
+       setorigin(this, targ.origin - this.view_ofs);
+
+       if(!(this.spawnflags & TRAIN_NEEDACTIVATION))
+       {
+               this.nextthink = this.ltime + 1;
+               setthink(this, train_next);
+       }
+
+       train_link(this);
+}
+
+#endif
+
+/*QUAKED spawnfunc_func_train (0 .5 .8) ?
+Ridable platform, targets spawnfunc_path_corner path to follow.
+speed : speed the train moves (can be overridden by each spawnfunc_path_corner)
+target : targetname of first spawnfunc_path_corner (starts here)
+*/
+#ifdef SVQC
+spawnfunc(func_train)
+{
+       if (this.noise != "")
+               precache_sound(this.noise);
+
+       if (this.target == "")
+               objerror(this, "func_train without a target");
+       if (!this.speed)
+               this.speed = 100;
+
+       if (!InitMovingBrushTrigger(this))
+               return;
+       this.effects |= EF_LOWPRECISION;
+
+       if(this.spawnflags & TRAIN_NEEDACTIVATION)
+               this.use = train_use;
+
+       if (this.spawnflags & TRAIN_TURN)
+       {
+               this.platmovetype_turn = true;
+               this.view_ofs = '0 0 0'; // don't offset a rotating train, origin works differently now
+       }
+       else
+               this.view_ofs = this.mins;
+
+       // wait for targets to spawn
+       InitializeEntity(this, func_train_find, INITPRIO_FINDTARGET);
+
+       setblocked(this, generic_plat_blocked);
+       if(this.dmg && (this.message == ""))
+               this.message = " was squished";
+    if(this.dmg && (this.message2 == ""))
+               this.message2 = "was squished by";
+       if(this.dmg && (!this.dmgtime))
+               this.dmgtime = 0.25;
+       this.dmgtime2 = time;
+
+       if(!set_platmovetype(this, this.platmovetype))
+               return;
+       this.platmovetype_start_default = this.platmovetype_start;
+       this.platmovetype_end_default = this.platmovetype_end;
+
+       // TODO make a reset function for this one
+}
+#elif defined(CSQC)
+void train_draw(entity this)
+{
+       //Movetype_Physics_NoMatchServer();
+       Movetype_Physics_MatchServer(this, autocvar_cl_projectiles_sloppy);
+}
+
+NET_HANDLE(ENT_CLIENT_TRAIN, bool isnew)
+{
+       float sf = ReadByte();
+
+       if(sf & SF_TRIGGER_INIT)
+       {
+               this.platmovetype = strzone(ReadString());
+               this.platmovetype_turn = ReadByte();
+               this.spawnflags = ReadByte();
+
+               this.model = strzone(ReadString());
+               _setmodel(this, this.model);
+
+               trigger_common_read(this, true);
+
+               this.curvetarget = strzone(ReadString());
+
+               this.pos1 = ReadVector();
+               this.pos2 = ReadVector();
+
+               this.size = ReadVector();
+
+               this.view_ofs = ReadVector();
+
+               this.mangle_x = ReadAngle();
+               this.mangle_y = ReadAngle();
+               this.mangle_z = ReadAngle();
+
+               this.speed = ReadShort();
+               this.height = ReadShort();
+               this.lip = ReadByte();
+               this.state = ReadByte();
+               this.wait = ReadByte();
+
+               this.dmg = ReadShort();
+               this.dmgtime = ReadByte();
+
+               this.classname = "func_train";
+               this.solid = SOLID_BSP;
+               set_movetype(this, MOVETYPE_PUSH);
+               this.drawmask = MASK_NORMAL;
+               this.draw = train_draw;
+               if (isnew) IL_PUSH(g_drawables, this);
+               this.entremove = trigger_remove_generic;
+
+               if(set_platmovetype(this, this.platmovetype))
+               {
+                       this.platmovetype_start_default = this.platmovetype_start;
+                       this.platmovetype_end_default = this.platmovetype_end;
+               }
+
+               // everything is set up by the time the train is linked, we shouldn't need this
+               //func_train_find();
+
+               // but we will need these
+               train_next(this);
+
+               set_movetype(this, MOVETYPE_PUSH);
+               this.move_time = time;
+       }
+
+       if(sf & SF_TRIGGER_RESET)
+       {
+               // TODO: make a reset function for trains
+       }
+
+       return true;
+}
+
+#endif
diff --git a/qcsrc/common/mapobjects/func/train.qh b/qcsrc/common/mapobjects/func/train.qh
new file mode 100644 (file)
index 0000000..0b2a099
--- /dev/null
@@ -0,0 +1,10 @@
+#pragma once
+
+
+const int TRAIN_CURVE = BIT(0);
+const int TRAIN_TURN = BIT(1);
+const int TRAIN_NEEDACTIVATION = BIT(2);
+
+#ifdef CSQC
+.float dmgtime;
+#endif
diff --git a/qcsrc/common/mapobjects/func/vectormamamam.qc b/qcsrc/common/mapobjects/func/vectormamamam.qc
new file mode 100644 (file)
index 0000000..61da52a
--- /dev/null
@@ -0,0 +1,194 @@
+#include "vectormamamam.qh"
+#ifdef SVQC
+// reusing some fields havocbots declared
+.entity wp00, wp01, wp02, wp03;
+
+.float targetfactor, target2factor, target3factor, target4factor;
+.vector targetnormal, target2normal, target3normal, target4normal;
+
+vector func_vectormamamam_origin(entity o, float timestep)
+{
+       vector v, p;
+       float flags;
+       entity e;
+
+       flags = o.spawnflags;
+       v = '0 0 0';
+
+       e = o.wp00;
+       if(e)
+       {
+               p = e.origin + timestep * e.velocity;
+               if(flags & PROJECT_ON_TARGETNORMAL)
+                       v = v + (p * o.targetnormal) * o.targetnormal * o.targetfactor;
+               else
+                       v = v + (p - (p * o.targetnormal) * o.targetnormal) * o.targetfactor;
+       }
+
+       e = o.wp01;
+       if(e)
+       {
+               p = e.origin + timestep * e.velocity;
+               if(flags & PROJECT_ON_TARGET2NORMAL)
+                       v = v + (p * o.target2normal) * o.target2normal * o.target2factor;
+               else
+                       v = v + (p - (p * o.target2normal) * o.target2normal) * o.target2factor;
+       }
+
+       e = o.wp02;
+       if(e)
+       {
+               p = e.origin + timestep * e.velocity;
+               if(flags & PROJECT_ON_TARGET3NORMAL)
+                       v = v + (p * o.target3normal) * o.target3normal * o.target3factor;
+               else
+                       v = v + (p - (p * o.target3normal) * o.target3normal) * o.target3factor;
+       }
+
+       e = o.wp03;
+       if(e)
+       {
+               p = e.origin + timestep * e.velocity;
+               if(flags & PROJECT_ON_TARGET4NORMAL)
+                       v = v + (p * o.target4normal) * o.target4normal * o.target4factor;
+               else
+                       v = v + (p - (p * o.target4normal) * o.target4normal) * o.target4factor;
+       }
+
+       return v;
+}
+
+void func_vectormamamam_controller_think(entity this)
+{
+       this.nextthink = time + vectormamamam_timestep;
+
+       if(this.owner.active != ACTIVE_ACTIVE)
+       {
+               this.owner.velocity = '0 0 0';
+               return;
+       }
+
+       if(this.owner.classname == "func_vectormamamam") // don't brake stuff if the func_vectormamamam was killtarget'ed
+               this.owner.velocity = (this.owner.destvec + func_vectormamamam_origin(this.owner, vectormamamam_timestep) - this.owner.origin) * 10;
+}
+
+void func_vectormamamam_findtarget(entity this)
+{
+       if(this.target != "")
+               this.wp00 = find(NULL, targetname, this.target);
+
+       if(this.target2 != "")
+               this.wp01 = find(NULL, targetname, this.target2);
+
+       if(this.target3 != "")
+               this.wp02 = find(NULL, targetname, this.target3);
+
+       if(this.target4 != "")
+               this.wp03 = find(NULL, targetname, this.target4);
+
+       if(!this.wp00 && !this.wp01 && !this.wp02 && !this.wp03)
+               objerror(this, "No reference entity found, so there is nothing to move. Aborting.");
+
+       this.destvec = this.origin - func_vectormamamam_origin(this, 0);
+
+       entity controller;
+       controller = new(func_vectormamamam_controller);
+       controller.owner = this;
+       controller.nextthink = time + 1;
+       setthink(controller, func_vectormamamam_controller_think);
+}
+
+void func_vectormamamam_setactive(entity this, int astate)
+{
+       if (astate == ACTIVE_TOGGLE)
+       {
+               if(this.active == ACTIVE_ACTIVE)
+                       this.active = ACTIVE_NOT;
+               else
+                       this.active = ACTIVE_ACTIVE;
+       }
+       else
+               this.active = astate;
+
+       if(this.active  == ACTIVE_NOT)
+       {
+               stopsound(this, CH_TRIGGER_SINGLE);
+       }
+       else
+       {
+               if(this.noise && this.noise != "")
+               {
+                       _sound(this, CH_TRIGGER_SINGLE, this.noise, VOL_BASE, ATTEN_IDLE);
+               }
+       }
+}
+
+void func_vectormamamam_init_for_player(entity this, entity player)
+{
+       if (this.noise && this.noise != "" && this.active == ACTIVE_ACTIVE && IS_REAL_CLIENT(player))
+       {
+               msg_entity = player;
+               soundto(MSG_ONE, this, CH_TRIGGER_SINGLE, this.noise, VOL_BASE, ATTEN_IDLE);
+       }
+}
+
+spawnfunc(func_vectormamamam)
+{
+       if (this.noise != "")
+       {
+               precache_sound(this.noise);
+       }
+
+       if(!this.targetfactor)
+               this.targetfactor = 1;
+
+       if(!this.target2factor)
+               this.target2factor = 1;
+
+       if(!this.target3factor)
+               this.target3factor = 1;
+
+       if(!this.target4factor)
+               this.target4factor = 1;
+
+       if(this.targetnormal)
+               this.targetnormal = normalize(this.targetnormal);
+
+       if(this.target2normal)
+               this.target2normal = normalize(this.target2normal);
+
+       if(this.target3normal)
+               this.target3normal = normalize(this.target3normal);
+
+       if(this.target4normal)
+               this.target4normal = normalize(this.target4normal);
+
+       setblocked(this, generic_plat_blocked);
+       if(this.dmg && (this.message == ""))
+               this.message = " was squished";
+    if(this.dmg && (this.message == ""))
+               this.message2 = "was squished by";
+       if(this.dmg && (!this.dmgtime))
+               this.dmgtime = 0.25;
+       this.dmgtime2 = time;
+
+       if (!InitMovingBrushTrigger(this))
+               return;
+
+       // wait for targets to spawn
+       this.nextthink = this.ltime + 999999999;
+       setthink(this, SUB_NullThink); // for PushMove
+
+       // Savage: Reduce bandwith, critical on e.g. nexdm02
+       this.effects |= EF_LOWPRECISION;
+
+       this.setactive = func_vectormamamam_setactive;
+       this.setactive(this, ACTIVE_ACTIVE);
+
+       // maybe send sound to new players
+       IL_PUSH(g_initforplayer, this);
+       this.init_for_player = func_vectormamamam_init_for_player;
+
+       InitializeEntity(this, func_vectormamamam_findtarget, INITPRIO_FINDTARGET);
+}
+#endif
diff --git a/qcsrc/common/mapobjects/func/vectormamamam.qh b/qcsrc/common/mapobjects/func/vectormamamam.qh
new file mode 100644 (file)
index 0000000..7eb6b0a
--- /dev/null
@@ -0,0 +1,9 @@
+#pragma once
+
+
+const int PROJECT_ON_TARGETNORMAL = BIT(0);
+const int PROJECT_ON_TARGET2NORMAL = BIT(1);
+const int PROJECT_ON_TARGET3NORMAL = BIT(2);
+const int PROJECT_ON_TARGET4NORMAL = BIT(3);
+
+const float vectormamamam_timestep = 0.1;
diff --git a/qcsrc/common/mapobjects/include.qc b/qcsrc/common/mapobjects/include.qc
new file mode 100644 (file)
index 0000000..59da1f2
--- /dev/null
@@ -0,0 +1,20 @@
+#include "include.qh"
+
+// some required common stuff
+#include "subs.qc"
+#include "triggers.qc"
+#include "platforms.qc"
+#include "teleporters.qc"
+
+// func
+#include "func/include.qc"
+
+// misc
+#include "misc/include.qc"
+
+// target
+#include "target/include.qc"
+
+// trigger
+#include "trigger/include.qc"
+
diff --git a/qcsrc/common/mapobjects/include.qh b/qcsrc/common/mapobjects/include.qh
new file mode 100644 (file)
index 0000000..87c07c1
--- /dev/null
@@ -0,0 +1,19 @@
+#pragma once
+
+// some required common stuff
+#ifdef SVQC
+       #include <server/item_key.qh>
+#endif
+#include "triggers.qh"
+#include "subs.qh"
+#include "platforms.qh"
+#include "teleporters.qh"
+
+// func
+#include "func/include.qh"
+
+// target
+#include "target/include.qh"
+
+// trigger
+#include "trigger/include.qh"
diff --git a/qcsrc/common/mapobjects/misc/_mod.inc b/qcsrc/common/mapobjects/misc/_mod.inc
new file mode 100644 (file)
index 0000000..d8d5ea6
--- /dev/null
@@ -0,0 +1,6 @@
+// generated file; do not modify
+#include <common/mapobjects/misc/corner.qc>
+#include <common/mapobjects/misc/follow.qc>
+#include <common/mapobjects/misc/include.qc>
+#include <common/mapobjects/misc/laser.qc>
+#include <common/mapobjects/misc/teleport_dest.qc>
diff --git a/qcsrc/common/mapobjects/misc/_mod.qh b/qcsrc/common/mapobjects/misc/_mod.qh
new file mode 100644 (file)
index 0000000..5fefbe4
--- /dev/null
@@ -0,0 +1,6 @@
+// generated file; do not modify
+#include <common/mapobjects/misc/corner.qh>
+#include <common/mapobjects/misc/follow.qh>
+#include <common/mapobjects/misc/include.qh>
+#include <common/mapobjects/misc/laser.qh>
+#include <common/mapobjects/misc/teleport_dest.qh>
diff --git a/qcsrc/common/mapobjects/misc/corner.qc b/qcsrc/common/mapobjects/misc/corner.qc
new file mode 100644 (file)
index 0000000..a0f67b7
--- /dev/null
@@ -0,0 +1,75 @@
+#include "corner.qh"
+REGISTER_NET_LINKED(ENT_CLIENT_CORNER)
+
+#ifdef SVQC
+bool corner_send(entity this, entity to, int sf)
+{
+       WriteHeader(MSG_ENTITY, ENT_CLIENT_CORNER);
+
+       WriteString(MSG_ENTITY, this.platmovetype);
+
+       WriteVector(MSG_ENTITY, this.origin);
+
+       WriteString(MSG_ENTITY, this.target);
+       WriteString(MSG_ENTITY, this.target2);
+       WriteString(MSG_ENTITY, this.target3);
+       WriteString(MSG_ENTITY, this.target4);
+       WriteString(MSG_ENTITY, this.targetname);
+       WriteByte(MSG_ENTITY, this.target_random);
+
+       WriteByte(MSG_ENTITY, this.wait);
+
+       return true;
+}
+
+void corner_link(entity this)
+{
+       //Net_LinkEntity(this, false, 0, corner_send);
+}
+
+spawnfunc(path_corner)
+{
+       // setup values for overriding train movement
+       // if a second value does not exist, both start and end speeds are the single value specified
+       set_platmovetype(this, this.platmovetype);
+
+       corner_link(this);
+}
+#elif defined(CSQC)
+
+void corner_remove(entity this)
+{
+       strfree(this.target);
+       strfree(this.target2);
+       strfree(this.target3);
+       strfree(this.target4);
+       strfree(this.targetname);
+       strfree(this.platmovetype);
+}
+
+NET_HANDLE(ENT_CLIENT_CORNER, bool isnew)
+{
+       this.platmovetype = strzone(ReadString());
+
+       this.origin = ReadVector();
+       setorigin(this, this.origin);
+
+       this.target = strzone(ReadString());
+       this.target2 = strzone(ReadString());
+       this.target3 = strzone(ReadString());
+       this.target4 = strzone(ReadString());
+       this.targetname = strzone(ReadString());
+       this.target_random = ReadByte();
+
+       this.wait = ReadByte();
+
+       return = true;
+
+       this.classname = "path_corner";
+       this.drawmask = MASK_NORMAL;
+       this.entremove = corner_remove;
+
+       set_platmovetype(this, this.platmovetype);
+}
+
+#endif
diff --git a/qcsrc/common/mapobjects/misc/corner.qh b/qcsrc/common/mapobjects/misc/corner.qh
new file mode 100644 (file)
index 0000000..6f70f09
--- /dev/null
@@ -0,0 +1 @@
+#pragma once
diff --git a/qcsrc/common/mapobjects/misc/follow.qc b/qcsrc/common/mapobjects/misc/follow.qc
new file mode 100644 (file)
index 0000000..87619ca
--- /dev/null
@@ -0,0 +1,70 @@
+#include "follow.qh"
+// the way this entity works makes it no use to CSQC, as it removes itself instantly
+
+#ifdef SVQC
+void follow_init(entity this)
+{
+       entity src, dst;
+       src = NULL;
+       dst = NULL;
+       if(this.killtarget != "")
+               src = find(NULL, targetname, this.killtarget);
+       if(this.target != "")
+               dst = find(NULL, targetname, this.target);
+
+       if(!src && !dst)
+       {
+               objerror(this, "follow: could not find target/killtarget");
+               return;
+       }
+
+       if(this.jointtype)
+       {
+               // already done :P entity must stay
+               this.aiment = src;
+               this.enemy = dst;
+       }
+       else if(!src || !dst)
+       {
+               objerror(this, "follow: could not find target/killtarget");
+               return;
+       }
+       else if(this.spawnflags & FOLLOW_ATTACH)
+       {
+               // attach
+               if(this.spawnflags & FOLLOW_LOCAL)
+               {
+                       setattachment(dst, src, this.message);
+               }
+               else
+               {
+                       attach_sameorigin(dst, src, this.message);
+               }
+
+               dst.solid = SOLID_NOT; // solid doesn't work with attachment
+               delete(this);
+       }
+       else
+       {
+               if(this.spawnflags & FOLLOW_LOCAL)
+               {
+                       set_movetype(dst, MOVETYPE_FOLLOW);
+                       dst.aiment = src;
+                       // dst.punchangle = '0 0 0'; // keep unchanged
+                       dst.view_ofs = dst.origin;
+                       dst.v_angle = dst.angles;
+               }
+               else
+               {
+                       follow_sameorigin(dst, src);
+               }
+
+               delete(this);
+       }
+}
+
+spawnfunc(misc_follow)
+{
+       InitializeEntity(this, follow_init, INITPRIO_FINDTARGET);
+}
+#endif
diff --git a/qcsrc/common/mapobjects/misc/follow.qh b/qcsrc/common/mapobjects/misc/follow.qh
new file mode 100644 (file)
index 0000000..aef491f
--- /dev/null
@@ -0,0 +1,5 @@
+#pragma once
+
+
+const int FOLLOW_ATTACH = BIT(0);
+const int FOLLOW_LOCAL = BIT(1);
diff --git a/qcsrc/common/mapobjects/misc/include.qc b/qcsrc/common/mapobjects/misc/include.qc
new file mode 100644 (file)
index 0000000..bbe5ce0
--- /dev/null
@@ -0,0 +1,5 @@
+#include "include.qh"
+#include "corner.qc"
+#include "follow.qc"
+#include "laser.qc"
+#include "teleport_dest.qc"
diff --git a/qcsrc/common/mapobjects/misc/include.qh b/qcsrc/common/mapobjects/misc/include.qh
new file mode 100644 (file)
index 0000000..6f70f09
--- /dev/null
@@ -0,0 +1 @@
+#pragma once
diff --git a/qcsrc/common/mapobjects/misc/laser.qc b/qcsrc/common/mapobjects/misc/laser.qc
new file mode 100644 (file)
index 0000000..df88b75
--- /dev/null
@@ -0,0 +1,418 @@
+#include "laser.qh"
+#if defined(CSQC)
+       #include <lib/csqcmodel/interpolate.qh>
+       #include <client/main.qh>
+       #include <lib/csqcmodel/cl_model.qh>
+#elif defined(MENUQC)
+#elif defined(SVQC)
+#endif
+
+REGISTER_NET_LINKED(ENT_CLIENT_LASER)
+
+#ifdef SVQC
+.float modelscale;
+void misc_laser_aim(entity this)
+{
+       vector a;
+       if(this.enemy)
+       {
+               if(this.spawnflags & LASER_FINITE)
+               {
+                       if(this.enemy.origin != this.mangle)
+                       {
+                               this.mangle = this.enemy.origin;
+                               this.SendFlags |= SF_LASER_UPDATE_TARGET;
+                       }
+               }
+               else
+               {
+                       a = vectoangles(this.enemy.origin - this.origin);
+                       a_x = -a_x;
+                       if(a != this.mangle)
+                       {
+                               this.mangle = a;
+                               this.SendFlags |= SF_LASER_UPDATE_TARGET;
+                       }
+               }
+       }
+       else
+       {
+               if(this.angles != this.mangle)
+               {
+                       this.mangle = this.angles;
+                       this.SendFlags |= SF_LASER_UPDATE_TARGET;
+               }
+       }
+       if(this.origin != this.oldorigin)
+       {
+               this.SendFlags |= SF_LASER_UPDATE_ORIGIN;
+               this.oldorigin = this.origin;
+       }
+}
+
+void misc_laser_init(entity this)
+{
+       if(this.target != "")
+               this.enemy = find(NULL, targetname, this.target);
+}
+
+.entity pusher;
+void misc_laser_think(entity this)
+{
+       vector o;
+       entity hitent;
+       vector hitloc;
+
+       this.nextthink = time;
+
+       if(this.active == ACTIVE_NOT)
+               return;
+
+       misc_laser_aim(this);
+
+       if(this.enemy)
+       {
+               o = this.enemy.origin;
+               if (!(this.spawnflags & LASER_FINITE))
+                       o = this.origin + normalize(o - this.origin) * LASER_BEAM_MAXLENGTH;
+       }
+       else
+       {
+               makevectors(this.mangle);
+               o = this.origin + v_forward * LASER_BEAM_MAXLENGTH;
+       }
+
+       if(this.dmg || this.enemy.target != "")
+       {
+               traceline(this.origin, o, MOVE_NORMAL, this);
+       }
+       hitent = trace_ent;
+       hitloc = trace_endpos;
+
+       if(this.enemy.target != "") // DETECTOR laser
+       {
+               if(trace_ent.iscreature)
+               {
+                       this.pusher = hitent;
+                       if(!this.count)
+                       {
+                               this.count = 1;
+
+                               SUB_UseTargets(this.enemy, this.enemy.pusher, NULL);
+                       }
+               }
+               else
+               {
+                       if(this.count)
+                       {
+                               this.count = 0;
+
+                               SUB_UseTargets(this.enemy, this.enemy.pusher, NULL);
+                       }
+               }
+       }
+
+       if(this.dmg)
+       {
+               if(this.team)
+                       if(((this.spawnflags & LASER_INVERT_TEAM) == 0) == (this.team != hitent.team))
+                               return;
+               if(hitent.takedamage)
+                       Damage(hitent, this, this, ((this.dmg < 0) ? 100000 : (this.dmg * frametime)), DEATH_HURTTRIGGER.m_id, DMG_NOWEP, hitloc, '0 0 0');
+       }
+}
+
+bool laser_SendEntity(entity this, entity to, float sendflags)
+{
+       WriteHeader(MSG_ENTITY, ENT_CLIENT_LASER);
+       sendflags = sendflags & 0x0F; // use that bit to indicate finite length laser
+       if(this.spawnflags & LASER_FINITE)
+               sendflags |= SF_LASER_FINITE;
+       if(this.alpha)
+               sendflags |= SF_LASER_ALPHA;
+       if(this.scale != 1 || this.modelscale != 1)
+               sendflags |= SF_LASER_SCALE;
+       if(this.spawnflags & LASER_NOTRACE)
+               sendflags |= SF_LASER_NOTRACE;
+       WriteByte(MSG_ENTITY, sendflags);
+       if(sendflags & SF_LASER_UPDATE_ORIGIN)
+       {
+               WriteVector(MSG_ENTITY, this.origin);
+       }
+       if(sendflags & SF_LASER_UPDATE_EFFECT)
+       {
+               WriteByte(MSG_ENTITY, this.beam_color.x * 255.0);
+               WriteByte(MSG_ENTITY, this.beam_color.y * 255.0);
+               WriteByte(MSG_ENTITY, this.beam_color.z * 255.0);
+               if(sendflags & SF_LASER_ALPHA)
+                       WriteByte(MSG_ENTITY, this.alpha * 255.0);
+               if(sendflags & SF_LASER_SCALE)
+               {
+                       WriteByte(MSG_ENTITY, bound(0, this.scale * 16.0, 255));
+                       WriteByte(MSG_ENTITY, bound(0, this.modelscale * 16.0, 255));
+               }
+               if((sendflags & SF_LASER_FINITE) || !(sendflags & SF_LASER_NOTRACE)) // effect doesn't need sending if the laser is infinite and has collision testing turned off
+                       WriteShort(MSG_ENTITY, this.cnt);
+       }
+       if(sendflags & SF_LASER_UPDATE_TARGET)
+       {
+               if(sendflags & SF_LASER_FINITE)
+               {
+                       WriteVector(MSG_ENTITY, this.enemy.origin);
+               }
+               else
+               {
+                       WriteAngle(MSG_ENTITY, this.mangle_x);
+                       WriteAngle(MSG_ENTITY, this.mangle_y);
+               }
+       }
+       if(sendflags & SF_LASER_UPDATE_ACTIVE)
+               WriteByte(MSG_ENTITY, this.active);
+       return true;
+}
+
+/*QUAKED spawnfunc_misc_laser (.5 .5 .5) ? START_ON DEST_IS_FIXED
+Any object touching the beam will be hurt
+Keys:
+"target"
+ spawnfunc_target_position where the laser ends
+"mdl"
+ name of beam end effect to use
+"beam_color"
+ color of the beam (default: red)
+"dmg"
+ damage per second (-1 for a laser that kills immediately)
+*/
+
+void laser_setactive(entity this, int act)
+{
+       int old_status = this.active;
+       if(act == ACTIVE_TOGGLE)
+       {
+               if(this.active == ACTIVE_ACTIVE)
+               {
+                       this.active = ACTIVE_NOT;
+               }
+               else
+               {
+                       this.active = ACTIVE_ACTIVE;
+               }
+       }
+       else
+       {
+               this.active = act;
+       }
+
+       if (this.active != old_status)
+       {
+               this.SendFlags |= SF_LASER_UPDATE_ACTIVE;
+               misc_laser_aim(this);
+       }
+}
+
+void laser_use(entity this, entity actor, entity trigger)
+{
+       this.setactive(this, ACTIVE_TOGGLE);
+}
+
+spawnfunc(misc_laser)
+{
+       if(this.mdl)
+       {
+               if(this.mdl == "none")
+                       this.cnt = -1;
+               else
+               {
+                       this.cnt = _particleeffectnum(this.mdl);
+                       if(this.cnt < 0 && this.dmg)
+                this.cnt = particleeffectnum(EFFECT_LASER_DEADLY);
+               }
+       }
+       else if(!this.cnt)
+       {
+               if(this.dmg)
+                       this.cnt = particleeffectnum(EFFECT_LASER_DEADLY);
+               else
+                       this.cnt = -1;
+       }
+       if(this.cnt < 0)
+               this.cnt = -1;
+
+       if(!this.beam_color && this.colormod)
+       {
+               LOG_WARN("misc_laser uses legacy field 'colormod', please use 'beam_color' instead");
+               this.beam_color = this.colormod;
+       }
+
+       if(this.beam_color == '0 0 0')
+       {
+               if(!this.alpha)
+                       this.beam_color = '1 0 0';
+       }
+
+       if(this.message == "")
+       {
+               this.message = "saw the light";
+       }
+       if (this.message2 == "")
+       {
+               this.message2 = "was pushed into a laser by";
+       }
+       if(!this.scale)
+       {
+               this.scale = 1;
+       }
+       if(!this.modelscale)
+       {
+               this.modelscale = 1;
+       }
+       else if(this.modelscale < 0)
+       {
+               this.modelscale = 0;
+       }
+       setthink(this, misc_laser_think);
+       this.nextthink = time;
+       InitializeEntity(this, misc_laser_init, INITPRIO_FINDTARGET);
+
+       this.mangle = this.angles;
+
+       Net_LinkEntity(this, false, 0, laser_SendEntity);
+
+       this.setactive = laser_setactive;
+
+       IFTARGETED
+       {
+               // backwards compatibility
+               this.use = laser_use;
+       }
+
+       this.reset = generic_netlinked_reset;
+       this.reset(this);
+}
+#elif defined(CSQC)
+
+// a laser goes from origin in direction angles
+// it has color 'beam_color'
+// and stops when something is in the way
+entityclass(Laser);
+classfield(Laser) .int cnt; // end effect
+classfield(Laser) .vector colormod;
+classfield(Laser) .int state; // on-off
+classfield(Laser) .int count; // flags for the laser
+classfield(Laser) .vector velocity; // laser endpoint if it is FINITE
+classfield(Laser) .float alpha;
+classfield(Laser) .float scale; // scaling factor of the thickness
+classfield(Laser) .float modelscale; // scaling factor of the dlight
+
+void Draw_Laser(entity this)
+{
+       if(this.active == ACTIVE_NOT)
+               return;
+       InterpolateOrigin_Do(this);
+       if(this.count & SF_LASER_FINITE)
+       {
+               if(this.count & SF_LASER_NOTRACE)
+               {
+                       trace_endpos = this.velocity;
+                       trace_dphitq3surfaceflags = 0;
+               }
+               else
+                       traceline(this.origin, this.velocity, 0, this);
+       }
+       else
+       {
+               if(this.count & SF_LASER_NOTRACE)
+               {
+                       makevectors(this.angles);
+                       trace_endpos = this.origin + v_forward * LASER_BEAM_MAXWORLDSIZE;
+                       trace_dphitq3surfaceflags = Q3SURFACEFLAG_SKY;
+               }
+               else
+               {
+                       makevectors(this.angles);
+                       traceline(this.origin, this.origin + v_forward * LASER_BEAM_MAXLENGTH, 0, this);
+                       if(trace_dphitq3surfaceflags & Q3SURFACEFLAG_SKY)
+                               trace_endpos = this.origin + v_forward * LASER_BEAM_MAXWORLDSIZE;
+               }
+       }
+       if(this.scale != 0)
+       {
+               if(this.alpha)
+               {
+                       Draw_CylindricLine(this.origin, trace_endpos, this.scale, "particles/laserbeam", 0, time * 3, this.beam_color, this.alpha, DRAWFLAG_NORMAL, view_origin);
+               }
+               else
+               {
+                       Draw_CylindricLine(this.origin, trace_endpos, this.scale, "particles/laserbeam", 0, time * 3, this.beam_color, 0.5, DRAWFLAG_ADDITIVE, view_origin);
+               }
+       }
+       if (!(trace_dphitq3surfaceflags & (Q3SURFACEFLAG_SKY | Q3SURFACEFLAG_NOIMPACT)))
+       {
+               if(this.cnt >= 0)
+                       __pointparticles(this.cnt, trace_endpos, trace_plane_normal, drawframetime * 1000);
+               if(this.beam_color != '0 0 0' && this.modelscale != 0)
+                       adddynamiclight(trace_endpos + trace_plane_normal * 1, this.modelscale, this.beam_color * 5);
+       }
+}
+
+NET_HANDLE(ENT_CLIENT_LASER, bool isnew)
+{
+       InterpolateOrigin_Undo(this);
+
+       // 30 bytes, or 13 bytes for just moving
+       int sendflags = ReadByte();
+       this.count = (sendflags & 0xF0);
+
+       if(this.count & SF_LASER_FINITE)
+               this.iflags = IFLAG_VELOCITY | IFLAG_ORIGIN;
+       else
+               this.iflags = IFLAG_ANGLES | IFLAG_ORIGIN;
+
+       if(sendflags & SF_LASER_UPDATE_ORIGIN)
+       {
+               this.origin = ReadVector();
+               setorigin(this, this.origin);
+       }
+       if(sendflags & SF_LASER_UPDATE_EFFECT)
+       {
+               this.beam_color.x = ReadByte() / 255.0;
+               this.beam_color.y = ReadByte() / 255.0;
+               this.beam_color.z = ReadByte() / 255.0;
+               if(sendflags & SF_LASER_ALPHA)
+                       this.alpha = ReadByte() / 255.0;
+               else
+                       this.alpha = 0;
+               this.scale = 2; // NOTE: why 2?
+               this.modelscale = 50; // NOTE: why 50?
+               if(sendflags & SF_LASER_SCALE)
+               {
+                       this.scale *= ReadByte() / 16.0; // beam radius
+                       this.modelscale *= ReadByte() / 16.0; // dlight radius
+               }
+               if((sendflags & SF_LASER_FINITE) || !(sendflags & SF_LASER_NOTRACE))
+                       this.cnt = ReadShort(); // effect number
+               else
+                       this.cnt = 0;
+       }
+       if(sendflags & SF_LASER_UPDATE_TARGET)
+       {
+               if(sendflags & SF_LASER_FINITE)
+               {
+                       this.velocity = ReadVector();
+               }
+               else
+               {
+                       this.angles_x = ReadAngle();
+                       this.angles_y = ReadAngle();
+               }
+       }
+       if(sendflags & SF_LASER_UPDATE_ACTIVE)
+               this.active = ReadByte();
+
+       return = true;
+
+       InterpolateOrigin_Note(this);
+       this.draw = Draw_Laser;
+       if (isnew) IL_PUSH(g_drawables, this);
+}
+#endif
diff --git a/qcsrc/common/mapobjects/misc/laser.qh b/qcsrc/common/mapobjects/misc/laser.qh
new file mode 100644 (file)
index 0000000..0ff5764
--- /dev/null
@@ -0,0 +1,22 @@
+#pragma once
+
+
+const int LASER_FINITE = BIT(1);
+const int LASER_NOTRACE = BIT(2);
+const int LASER_INVERT_TEAM = BIT(3);
+
+const int SF_LASER_UPDATE_ORIGIN = BIT(0);
+const int SF_LASER_UPDATE_TARGET = BIT(1);
+const int SF_LASER_UPDATE_ACTIVE = BIT(2);
+const int SF_LASER_UPDATE_EFFECT = BIT(3);
+
+const int SF_LASER_NOTRACE = BIT(4);
+const int SF_LASER_SCALE = BIT(5);
+const int SF_LASER_ALPHA = BIT(6);
+const int SF_LASER_FINITE = BIT(7);
+
+.vector beam_color;
+
+const float LASER_BEAM_MAXLENGTH = 32768; // maximum length of a beam trace
+// TODO: find a better way to do this
+const float LASER_BEAM_MAXWORLDSIZE = 1048576; // to make sure the endpoint of the beam is not visible inside
diff --git a/qcsrc/common/mapobjects/misc/teleport_dest.qc b/qcsrc/common/mapobjects/misc/teleport_dest.qc
new file mode 100644 (file)
index 0000000..126a20e
--- /dev/null
@@ -0,0 +1,89 @@
+#include "teleport_dest.qh"
+REGISTER_NET_LINKED(ENT_CLIENT_TELEPORT_DEST)
+
+#ifdef SVQC
+
+bool teleport_dest_send(entity this, entity to, int sendflags)
+{
+       WriteHeader(MSG_ENTITY, ENT_CLIENT_TELEPORT_DEST);
+       WriteByte(MSG_ENTITY, sendflags);
+
+       if(sendflags & SF_TRIGGER_INIT)
+       {
+               WriteByte(MSG_ENTITY, this.cnt);
+               WriteCoord(MSG_ENTITY, this.speed);
+               WriteString(MSG_ENTITY, this.targetname);
+               WriteVector(MSG_ENTITY, this.origin);
+
+               WriteAngle(MSG_ENTITY, this.mangle_x);
+               WriteAngle(MSG_ENTITY, this.mangle_y);
+               WriteAngle(MSG_ENTITY, this.mangle_z);
+       }
+
+       return true;
+}
+
+void teleport_dest_link(entity this)
+{
+       Net_LinkEntity(this, false, 0, teleport_dest_send);
+       this.SendFlags |= SF_TRIGGER_INIT;
+}
+
+spawnfunc(info_teleport_destination)
+{
+       this.classname = "info_teleport_destination";
+
+       this.mangle = this.angles;
+       this.angles = '0 0 0';
+
+       //setorigin(this, this.origin + '0 0 27');      // To fix a mappers' habit as old as Quake
+       setorigin(this, this.origin);
+
+       IFTARGETED
+       {
+       }
+       else
+               objerror (this, "^3Teleport destination without a targetname");
+
+       teleport_dest_link(this);
+}
+
+spawnfunc(misc_teleporter_dest)
+{
+       spawnfunc_info_teleport_destination(this);
+}
+
+#elif defined(CSQC)
+
+void teleport_dest_remove(entity this)
+{
+    // strfree(this.classname);
+    strfree(this.targetname);
+}
+
+NET_HANDLE(ENT_CLIENT_TELEPORT_DEST, bool isnew)
+{
+       int sendflags = ReadByte();
+
+       if(sendflags & SF_TRIGGER_INIT)
+       {
+               this.classname = "info_teleport_destination";
+               this.cnt = ReadByte();
+               this.speed = ReadCoord();
+               this.targetname = strzone(ReadString());
+               this.origin = ReadVector();
+
+               this.mangle_x = ReadAngle();
+               this.mangle_y = ReadAngle();
+               this.mangle_z = ReadAngle();
+
+               setorigin(this, this.origin);
+
+               this.drawmask = MASK_NORMAL;
+               this.entremove = teleport_dest_remove;
+       }
+
+       return = true;
+}
+
+#endif
diff --git a/qcsrc/common/mapobjects/misc/teleport_dest.qh b/qcsrc/common/mapobjects/misc/teleport_dest.qh
new file mode 100644 (file)
index 0000000..6f70f09
--- /dev/null
@@ -0,0 +1 @@
+#pragma once
diff --git a/qcsrc/common/mapobjects/platforms.qc b/qcsrc/common/mapobjects/platforms.qc
new file mode 100644 (file)
index 0000000..4747877
--- /dev/null
@@ -0,0 +1,227 @@
+#include "platforms.qh"
+void generic_plat_blocked(entity this, entity blocker)
+{
+#ifdef SVQC
+       if(this.dmg && blocker.takedamage != DAMAGE_NO)
+       {
+               if(this.dmgtime2 < time)
+               {
+                       Damage (blocker, this, this, this.dmg, DEATH_HURTTRIGGER.m_id, DMG_NOWEP, blocker.origin, '0 0 0');
+                       this.dmgtime2 = time + this.dmgtime;
+               }
+
+               // Gib dead/dying stuff
+               if(IS_DEAD(blocker))
+                       Damage (blocker, this, this, 10000, DEATH_HURTTRIGGER.m_id, DMG_NOWEP, blocker.origin, '0 0 0');
+       }
+#endif
+}
+
+void plat_spawn_inside_trigger(entity this)
+{
+       entity trigger;
+       vector tmin, tmax;
+
+       trigger = spawn();
+       settouch(trigger, plat_center_touch);
+       set_movetype(trigger, MOVETYPE_NONE);
+       trigger.solid = SOLID_TRIGGER;
+       trigger.enemy = this;
+
+       tmin = this.absmin + '25 25 0';
+       tmax = this.absmax - '25 25 -8';
+       tmin_z = tmax_z - (this.pos1_z - this.pos2_z + 8);
+       if (this.spawnflags & PLAT_LOW_TRIGGER)
+               tmax_z = tmin_z + 8;
+
+       if (this.size_x <= 50)
+       {
+               tmin_x = (this.mins_x + this.maxs_x) / 2;
+               tmax_x = tmin_x + 1;
+       }
+       if (this.size_y <= 50)
+       {
+               tmin_y = (this.mins_y + this.maxs_y) / 2;
+               tmax_y = tmin_y + 1;
+       }
+
+       if(tmin_x < tmax_x)
+               if(tmin_y < tmax_y)
+                       if(tmin_z < tmax_z)
+                       {
+                               setsize (trigger, tmin, tmax);
+                               return;
+                       }
+
+       // otherwise, something is fishy...
+       delete(trigger);
+       objerror(this, "plat_spawn_inside_trigger: platform has odd size or lip, can't spawn");
+}
+
+void plat_hit_top(entity this)
+{
+       _sound (this, CH_TRIGGER_SINGLE, this.noise1, VOL_BASE, ATTEN_NORM);
+       this.state = STATE_TOP;
+
+       setthink(this, plat_go_down);
+       this.nextthink = this.ltime + 3;
+}
+
+void plat_hit_bottom(entity this)
+{
+       _sound (this, CH_TRIGGER_SINGLE, this.noise1, VOL_BASE, ATTEN_NORM);
+       this.state = STATE_BOTTOM;
+}
+
+void plat_go_down(entity this)
+{
+       _sound (this, CH_TRIGGER_SINGLE, this.noise, VOL_BASE, ATTEN_NORM);
+       this.state = STATE_DOWN;
+       SUB_CalcMove (this, this.pos2, TSPEED_LINEAR, this.speed, plat_hit_bottom);
+}
+
+void plat_go_up(entity this)
+{
+       _sound (this, CH_TRIGGER_SINGLE, this.noise, VOL_BASE, ATTEN_NORM);
+       this.state = STATE_UP;
+       SUB_CalcMove (this, this.pos1, TSPEED_LINEAR, this.speed, plat_hit_top);
+}
+
+void plat_center_touch(entity this, entity toucher)
+{
+#ifdef SVQC
+       if (!toucher.iscreature)
+               return;
+
+       if (toucher.health <= 0)
+               return;
+#elif defined(CSQC)
+       if (!IS_PLAYER(toucher))
+               return;
+       if(IS_DEAD(toucher))
+               return;
+#endif
+
+       if (this.enemy.state == STATE_BOTTOM) {
+               plat_go_up(this.enemy);
+       } else if (this.enemy.state == STATE_TOP)
+               this.enemy.nextthink = this.enemy.ltime + 1;
+}
+
+void plat_outside_touch(entity this, entity toucher)
+{
+#ifdef SVQC
+       if (!toucher.iscreature)
+               return;
+
+       if (toucher.health <= 0)
+               return;
+#elif defined(CSQC)
+       if (!IS_PLAYER(toucher))
+               return;
+#endif
+
+       if (this.enemy.state == STATE_TOP) {
+           entity e = this.enemy;
+               plat_go_down(e);
+    }
+}
+
+void plat_trigger_use(entity this, entity actor, entity trigger)
+{
+       if (getthink(this))
+               return;         // already activated
+       plat_go_down(this);
+}
+
+
+void plat_crush(entity this, entity blocker)
+{
+       if((this.spawnflags & CRUSH) && (blocker.takedamage != DAMAGE_NO))
+       { // KIll Kill Kill!!
+#ifdef SVQC
+               Damage (blocker, this, this, 10000, DEATH_HURTTRIGGER.m_id, DMG_NOWEP, blocker.origin, '0 0 0');
+#endif
+       }
+       else
+       {
+#ifdef SVQC
+               if((this.dmg) && (blocker.takedamage != DAMAGE_NO))
+               {   // Shall we bite?
+                       Damage (blocker, this, this, this.dmg, DEATH_HURTTRIGGER.m_id, DMG_NOWEP, blocker.origin, '0 0 0');
+                       // Gib dead/dying stuff
+                       if(IS_DEAD(blocker))
+                               Damage (blocker, this, this, 10000, DEATH_HURTTRIGGER.m_id, DMG_NOWEP, blocker.origin, '0 0 0');
+               }
+#endif
+
+               if (this.state == STATE_UP)
+                       plat_go_down (this);
+               else if (this.state == STATE_DOWN)
+                       plat_go_up (this);
+       // when in other states, then the plat_crush event came delayed after
+       // plat state already had changed
+       // this isn't a bug per se!
+       }
+}
+
+void plat_use(entity this, entity actor, entity trigger)
+{
+       this.use = func_null;
+       if (this.state != STATE_UP)
+               objerror (this, "plat_use: not in up state");
+       plat_go_down(this);
+}
+
+// WARNING: backwards compatibility because people don't use already existing fields :(
+// TODO: Check if any maps use these fields and remove these fields if it doesn't break maps
+.string sound1, sound2;
+
+void plat_reset(entity this)
+{
+       IFTARGETED
+       {
+               setorigin(this, this.pos1);
+               this.state = STATE_UP;
+               this.use = plat_use;
+       }
+       else
+       {
+               setorigin(this, this.pos2);
+               this.state = STATE_BOTTOM;
+               this.use = plat_trigger_use;
+       }
+
+#ifdef SVQC
+       this.SendFlags |= SF_TRIGGER_RESET;
+#endif
+}
+
+.float platmovetype_start_default, platmovetype_end_default;
+bool set_platmovetype(entity e, string s)
+{
+       // sets platmovetype_start and platmovetype_end based on a string consisting of two values
+
+       int n = tokenize_console(s);
+       if(n > 0)
+               e.platmovetype_start = stof(argv(0));
+       else
+               e.platmovetype_start = 0;
+
+       if(n > 1)
+               e.platmovetype_end = stof(argv(1));
+       else
+               e.platmovetype_end = e.platmovetype_start;
+
+       if(n > 2)
+               if(argv(2) == "force")
+                       return true; // no checking, return immediately
+
+       if(!cubic_speedfunc_is_sane(e.platmovetype_start, e.platmovetype_end))
+       {
+               objerror(e, "Invalid platform move type; platform would go in reverse, which is not allowed.");
+               return false;
+       }
+
+       return true;
+}
diff --git a/qcsrc/common/mapobjects/platforms.qh b/qcsrc/common/mapobjects/platforms.qh
new file mode 100644 (file)
index 0000000..346cebc
--- /dev/null
@@ -0,0 +1,15 @@
+#pragma once
+
+
+const int PLAT_LOW_TRIGGER = BIT(0);
+
+.float dmgtime2;
+
+void plat_center_touch(entity this, entity toucher);
+void plat_outside_touch(entity this, entity toucher);
+void plat_trigger_use(entity this, entity actor, entity trigger);
+void plat_go_up(entity this);
+void plat_go_down(entity this);
+void plat_crush(entity this, entity blocker);
+
+.float dmg;
diff --git a/qcsrc/common/mapobjects/subs.qc b/qcsrc/common/mapobjects/subs.qc
new file mode 100644 (file)
index 0000000..2a237fd
--- /dev/null
@@ -0,0 +1,369 @@
+#include "subs.qh"
+void SUB_NullThink(entity this) { }
+
+void SUB_CalcMoveDone(entity this);
+void SUB_CalcAngleMoveDone(entity this);
+
+/*
+==================
+SUB_Friction
+
+Applies some friction to this
+==================
+*/
+.float friction;
+void SUB_Friction (entity this)
+{
+       this.nextthink = time;
+       if(IS_ONGROUND(this))
+               this.velocity = this.velocity * (1 - frametime * this.friction);
+}
+
+/*
+==================
+SUB_VanishOrRemove
+
+Makes client invisible or removes non-client
+==================
+*/
+void SUB_VanishOrRemove (entity ent)
+{
+       if (IS_CLIENT(ent))
+       {
+               // vanish
+               ent.alpha = -1;
+               ent.effects = 0;
+#ifdef SVQC
+               ent.glow_size = 0;
+               ent.pflags = 0;
+#endif
+       }
+       else
+       {
+               // remove
+               delete(ent);
+       }
+}
+
+void SUB_SetFade_Think (entity this)
+{
+       if(this.alpha == 0)
+               this.alpha = 1;
+       setthink(this, SUB_SetFade_Think);
+       this.nextthink = time;
+       this.alpha -= frametime * this.fade_rate;
+       if (this.alpha < 0.01)
+               SUB_VanishOrRemove(this);
+       else
+               this.nextthink = time;
+}
+
+/*
+==================
+SUB_SetFade
+
+Fade 'ent' out when time >= 'when'
+==================
+*/
+void SUB_SetFade (entity ent, float when, float fading_time)
+{
+       ent.fade_rate = 1/fading_time;
+       setthink(ent, SUB_SetFade_Think);
+       ent.nextthink = when;
+}
+
+/*
+=============
+SUB_CalcMove
+
+calculate this.velocity and this.nextthink to reach dest from
+this.origin traveling at speed
+===============
+*/
+void SUB_CalcMoveDone(entity this)
+{
+       // After moving, set origin to exact final destination
+
+       setorigin (this, this.finaldest);
+       this.velocity = '0 0 0';
+       this.nextthink = -1;
+       if (this.think1 && this.think1 != SUB_CalcMoveDone)
+               this.think1 (this);
+}
+
+.float platmovetype_turn;
+void SUB_CalcMove_controller_think (entity this)
+{
+       float traveltime;
+       float phasepos;
+       float nexttick;
+       vector delta;
+       vector delta2;
+       vector veloc;
+       vector angloc;
+       vector nextpos;
+       delta = this.destvec;
+       delta2 = this.destvec2;
+       if(time < this.animstate_endtime)
+       {
+               nexttick = time + PHYS_INPUT_FRAMETIME;
+
+               traveltime = this.animstate_endtime - this.animstate_starttime;
+               phasepos = (nexttick - this.animstate_starttime) / traveltime; // range: [0, 1]
+               phasepos = cubic_speedfunc(this.platmovetype_start, this.platmovetype_end, phasepos);
+               nextpos = this.origin + (delta * phasepos) + (delta2 * phasepos * phasepos);
+               // derivative: delta + 2 * delta2 * phasepos (e.g. for angle positioning)
+
+               if(this.owner.platmovetype_turn)
+               {
+                       vector destangle;
+                       destangle = delta + 2 * delta2 * phasepos;
+                       destangle = vectoangles(destangle);
+                       destangle_x = -destangle_x; // flip up / down orientation
+
+                       // take the shortest distance for the angles
+                       vector v = this.owner.angles;
+                       v.x -= 360 * floor((v.x - destangle_x) / 360 + 0.5);
+                       v.y -= 360 * floor((v.y - destangle_y) / 360 + 0.5);
+                       v.z -= 360 * floor((v.z - destangle_z) / 360 + 0.5);
+                       this.owner.angles = v;
+                       angloc = destangle - this.owner.angles;
+                       angloc = angloc * (1 / PHYS_INPUT_FRAMETIME); // so it arrives for the next frame
+                       this.owner.avelocity = angloc;
+               }
+               if(nexttick < this.animstate_endtime)
+                       veloc = nextpos - this.owner.origin;
+               else
+                       veloc = this.finaldest - this.owner.origin;
+               veloc = veloc * (1 / PHYS_INPUT_FRAMETIME); // so it arrives for the next frame
+
+               this.owner.velocity = veloc;
+               this.nextthink = nexttick;
+       }
+       else
+       {
+               // derivative: delta + 2 * delta2 (e.g. for angle positioning)
+               entity own = this.owner;
+               setthink(own, this.think1);
+               // set the owner's reference to this entity to NULL
+               own.move_controller = NULL;
+               delete(this);
+               getthink(own)(own);
+       }
+}
+
+void SUB_CalcMove_controller_setbezier (entity controller, vector org, vector control, vector destin)
+{
+       // 0 * (1-t) * (1-t) + 2 * control * t * (1-t) + destin * t * t
+       // 2 * control * t - 2 * control * t * t + destin * t * t
+       // 2 * control * t + (destin - 2 * control) * t * t
+
+       setorigin(controller, org);
+       control -= org;
+       destin -= org;
+
+       controller.destvec = 2 * control; // control point
+       controller.destvec2 = destin - 2 * control; // quadratic part required to reach end point
+       // also: initial d/dphasepos origin = 2 * control, final speed = 2 * (destin - control)
+}
+
+void SUB_CalcMove_controller_setlinear (entity controller, vector org, vector destin)
+{
+       // 0 * (1-t) * (1-t) + 2 * control * t * (1-t) + destin * t * t
+       // 2 * control * t - 2 * control * t * t + destin * t * t
+       // 2 * control * t + (destin - 2 * control) * t * t
+
+       setorigin(controller, org);
+       destin -= org;
+
+       controller.destvec = destin; // end point
+       controller.destvec2 = '0 0 0';
+}
+
+float TSPEED_TIME = -1;
+float TSPEED_LINEAR = 0;
+float TSPEED_START = 1;
+float TSPEED_END = 2;
+// TODO average too?
+
+void SUB_CalcMove_Bezier (entity this, vector tcontrol, vector tdest, float tspeedtype, float tspeed, void(entity this) func)
+{
+       float   traveltime;
+       entity controller;
+
+       if (!tspeed)
+               objerror (this, "No speed is defined!");
+
+       this.think1 = func;
+       this.finaldest = tdest;
+       setthink(this, SUB_CalcMoveDone);
+
+       switch(tspeedtype)
+       {
+               default:
+               case TSPEED_START:
+                       traveltime = 2 * vlen(tcontrol - this.origin) / tspeed;
+                       break;
+               case TSPEED_END:
+                       traveltime = 2 * vlen(tcontrol - tdest)       / tspeed;
+                       break;
+               case TSPEED_LINEAR:
+                       traveltime = vlen(tdest - this.origin)        / tspeed;
+                       break;
+               case TSPEED_TIME:
+                       traveltime = tspeed;
+                       break;
+       }
+
+       if (traveltime < 0.1) // useless anim
+       {
+               this.velocity = '0 0 0';
+               this.nextthink = this.ltime + 0.1;
+               return;
+       }
+
+       // delete the previous controller, otherwise changing movement midway is glitchy
+       if (this.move_controller != NULL)
+       {
+               delete(this.move_controller);
+       }
+       controller = new(SUB_CalcMove_controller);
+       controller.owner = this;
+       this.move_controller = controller;
+       controller.platmovetype = this.platmovetype;
+       controller.platmovetype_start = this.platmovetype_start;
+       controller.platmovetype_end = this.platmovetype_end;
+       SUB_CalcMove_controller_setbezier(controller, this.origin, tcontrol, tdest);
+       controller.finaldest = (tdest + '0 0 0.125'); // where do we want to end? Offset to overshoot a bit.
+       controller.animstate_starttime = time;
+       controller.animstate_endtime = time + traveltime;
+       setthink(controller, SUB_CalcMove_controller_think);
+       controller.think1 = getthink(this);
+
+       // the thinking is now done by the controller
+       setthink(this, SUB_NullThink); // for PushMove
+       this.nextthink = this.ltime + traveltime;
+
+       // invoke controller
+       getthink(controller)(controller);
+}
+
+void SUB_CalcMove (entity this, vector tdest, float tspeedtype, float tspeed, void(entity this) func)
+{
+       vector  delta;
+       float   traveltime;
+
+       if (!tspeed)
+               objerror (this, "No speed is defined!");
+
+       this.think1 = func;
+       this.finaldest = tdest;
+       setthink(this, SUB_CalcMoveDone);
+
+       if (tdest == this.origin)
+       {
+               this.velocity = '0 0 0';
+               this.nextthink = this.ltime + 0.1;
+               return;
+       }
+
+       delta = tdest - this.origin;
+
+       switch(tspeedtype)
+       {
+               default:
+               case TSPEED_START:
+               case TSPEED_END:
+               case TSPEED_LINEAR:
+                       traveltime = vlen (delta) / tspeed;
+                       break;
+               case TSPEED_TIME:
+                       traveltime = tspeed;
+                       break;
+       }
+
+       // Very short animations don't really show off the effect
+       // of controlled animation, so let's just use linear movement.
+       // Alternatively entities can choose to specify non-controlled movement.
+        // The only currently implemented alternative movement is linear (value 1)
+       if (traveltime < 0.15 || (this.platmovetype_start == 1 && this.platmovetype_end == 1)) // is this correct?
+       {
+               this.velocity = delta * (1/traveltime); // QuakeC doesn't allow vector/float division
+               this.nextthink = this.ltime + traveltime;
+               return;
+       }
+
+       // now just run like a bezier curve...
+       SUB_CalcMove_Bezier(this, (this.origin + tdest) * 0.5, tdest, tspeedtype, tspeed, func);
+}
+
+void SUB_CalcMoveEnt (entity ent, vector tdest, float tspeedtype, float tspeed, void(entity this) func)
+{
+       SUB_CalcMove(ent, tdest, tspeedtype, tspeed, func);
+}
+
+/*
+=============
+SUB_CalcAngleMove
+
+calculate this.avelocity and this.nextthink to reach destangle from
+this.angles rotating
+
+The calling function should make sure this.setthink is valid
+===============
+*/
+void SUB_CalcAngleMoveDone(entity this)
+{
+       // After rotating, set angle to exact final angle
+       this.angles = this.finalangle;
+       this.avelocity = '0 0 0';
+       this.nextthink = -1;
+       if (this.think1 && this.think1 != SUB_CalcAngleMoveDone) // avoid endless loops
+               this.think1 (this);
+}
+
+// FIXME: I fixed this function only for rotation around the main axes
+void SUB_CalcAngleMove (entity this, vector destangle, float tspeedtype, float tspeed, void(entity this) func)
+{
+       if (!tspeed)
+               objerror (this, "No speed is defined!");
+
+       // take the shortest distance for the angles
+       this.angles_x -= 360 * floor((this.angles_x - destangle_x) / 360 + 0.5);
+       this.angles_y -= 360 * floor((this.angles_y - destangle_y) / 360 + 0.5);
+       this.angles_z -= 360 * floor((this.angles_z - destangle_z) / 360 + 0.5);
+       vector delta = destangle - this.angles;
+       float traveltime;
+
+       switch(tspeedtype)
+       {
+               default:
+               case TSPEED_START:
+               case TSPEED_END:
+               case TSPEED_LINEAR:
+                       traveltime = vlen (delta) / tspeed;
+                       break;
+               case TSPEED_TIME:
+                       traveltime = tspeed;
+                       break;
+       }
+
+       this.think1 = func;
+       this.finalangle = destangle;
+       setthink(this, SUB_CalcAngleMoveDone);
+
+       if (traveltime < 0.1)
+       {
+               this.avelocity = '0 0 0';
+               this.nextthink = this.ltime + 0.1;
+               return;
+       }
+
+       this.avelocity = delta * (1 / traveltime);
+       this.nextthink = this.ltime + traveltime;
+}
+
+void SUB_CalcAngleMoveEnt (entity ent, vector destangle, float tspeedtype, float tspeed, void(entity this) func)
+{
+       SUB_CalcAngleMove (ent, destangle, tspeedtype, tspeed, func);
+}
diff --git a/qcsrc/common/mapobjects/subs.qh b/qcsrc/common/mapobjects/subs.qh
new file mode 100644 (file)
index 0000000..8d4e406
--- /dev/null
@@ -0,0 +1,57 @@
+#pragma once
+#include "defs.qh"
+
+void SUB_SetFade (entity ent, float when, float fading_time);
+void SUB_VanishOrRemove (entity ent);
+
+.vector                finaldest, finalangle;          //plat.qc stuff
+.void(entity this) think1;
+.float state;
+.float         t_length, t_width;
+
+.vector destvec;
+.vector destvec2;
+
+.float delay;
+.float wait;
+.float lip;
+.float speed;
+.float sounds;
+.string  platmovetype;
+.float platmovetype_start, platmovetype_end;
+
+//entity activator;
+
+.string killtarget;
+
+.vector        pos1, pos2;
+.vector        mangle;
+
+.string target2;
+.string target3;
+.string target4;
+.string curvetarget;
+.float target_random;
+.float trigger_reverse;
+
+// Keys player is holding
+.float itemkeys;
+// message delay for func_door locked by keys and key locks
+// this field is used on player entities
+.float key_door_messagetime;
+
+.vector dest1, dest2;
+
+.entity move_controller;
+
+#ifdef CSQC
+// this stuff is defined in the server side engine VM, so we must define it separately here
+.float takedamage;
+const int DAMAGE_NO = 0;
+const int DAMAGE_YES = 1;
+const int DAMAGE_AIM = 2;
+
+.string                noise, noise1, noise2, noise3;  // contains names of wavs to play
+
+.float         max_health;             // players maximum health is stored here
+#endif
diff --git a/qcsrc/common/mapobjects/target/_mod.inc b/qcsrc/common/mapobjects/target/_mod.inc
new file mode 100644 (file)
index 0000000..5ac26d9
--- /dev/null
@@ -0,0 +1,11 @@
+// generated file; do not modify
+#include <common/mapobjects/target/changelevel.qc>
+#include <common/mapobjects/target/include.qc>
+#include <common/mapobjects/target/kill.qc>
+#include <common/mapobjects/target/levelwarp.qc>
+#include <common/mapobjects/target/location.qc>
+#include <common/mapobjects/target/music.qc>
+#include <common/mapobjects/target/spawn.qc>
+#include <common/mapobjects/target/spawnpoint.qc>
+#include <common/mapobjects/target/speaker.qc>
+#include <common/mapobjects/target/voicescript.qc>
diff --git a/qcsrc/common/mapobjects/target/_mod.qh b/qcsrc/common/mapobjects/target/_mod.qh
new file mode 100644 (file)
index 0000000..80a1a7f
--- /dev/null
@@ -0,0 +1,11 @@
+// generated file; do not modify
+#include <common/mapobjects/target/changelevel.qh>
+#include <common/mapobjects/target/include.qh>
+#include <common/mapobjects/target/kill.qh>
+#include <common/mapobjects/target/levelwarp.qh>
+#include <common/mapobjects/target/location.qh>
+#include <common/mapobjects/target/music.qh>
+#include <common/mapobjects/target/spawn.qh>
+#include <common/mapobjects/target/spawnpoint.qh>
+#include <common/mapobjects/target/speaker.qh>
+#include <common/mapobjects/target/voicescript.qh>
diff --git a/qcsrc/common/mapobjects/target/changelevel.qc b/qcsrc/common/mapobjects/target/changelevel.qc
new file mode 100644 (file)
index 0000000..114fd87
--- /dev/null
@@ -0,0 +1,55 @@
+#include "changelevel.qh"
+#ifdef SVQC
+.string chmap, gametype;
+.entity chlevel_targ;
+
+void target_changelevel_use(entity this, entity actor, entity trigger)
+{
+       if(this.spawnflags & CHANGELEVEL_MULTIPLAYER)
+       {
+               // simply don't react if a non-player triggers it
+               if(!IS_PLAYER(actor)) { return; }
+
+               actor.chlevel_targ = this;
+
+               int plnum = 0;
+               int realplnum = 0;
+               // let's not count bots
+               FOREACH_CLIENT(IS_PLAYER(it) && IS_REAL_CLIENT(it), {
+                       ++realplnum;
+                       if(it.chlevel_targ == this)
+                               ++plnum;
+               });
+               if(plnum < ceil(realplnum * min(1, this.count))) // 70% of players
+                       return;
+       }
+
+       if(this.gametype != "")
+               MapInfo_SwitchGameType(MapInfo_Type_FromString(this.gametype));
+
+       if (this.chmap == "")
+               localcmd("endmatch\n");
+       else
+               localcmd(strcat("changelevel ", this.chmap, "\n"));
+}
+
+/*target_changelevel
+Target to change/end level
+KEYS:
+chmap: map to switch to, leave empty for endmatch
+gametype: gametype for the next map
+count: fraction of real players that need to trigger this entity for levelchange
+SPAWNFLAGS:
+CHANGELEVEL_MULTIPLAYER: multiplayer support
+*/
+
+spawnfunc(target_changelevel)
+{
+       this.use = target_changelevel_use;
+
+       if(!this.count)
+       {
+               this.count = 0.7;
+       }
+}
+#endif
diff --git a/qcsrc/common/mapobjects/target/changelevel.qh b/qcsrc/common/mapobjects/target/changelevel.qh
new file mode 100644 (file)
index 0000000..f6e206e
--- /dev/null
@@ -0,0 +1,4 @@
+#pragma once
+
+
+const int CHANGELEVEL_MULTIPLAYER = BIT(1);
diff --git a/qcsrc/common/mapobjects/target/include.qc b/qcsrc/common/mapobjects/target/include.qc
new file mode 100644 (file)
index 0000000..a45c65e
--- /dev/null
@@ -0,0 +1,11 @@
+#include "include.qh"
+
+#include "changelevel.qc"
+#include "kill.qc"
+#include "levelwarp.qc"
+#include "location.qc"
+#include "music.qc"
+#include "spawn.qc"
+#include "spawnpoint.qc"
+#include "speaker.qc"
+#include "voicescript.qc"
diff --git a/qcsrc/common/mapobjects/target/include.qh b/qcsrc/common/mapobjects/target/include.qh
new file mode 100644 (file)
index 0000000..c0f7cad
--- /dev/null
@@ -0,0 +1,3 @@
+#pragma once
+
+#include "music.qh"
diff --git a/qcsrc/common/mapobjects/target/kill.qc b/qcsrc/common/mapobjects/target/kill.qc
new file mode 100644 (file)
index 0000000..2751c60
--- /dev/null
@@ -0,0 +1,26 @@
+#include "kill.qh"
+#include "location.qh"
+#ifdef SVQC
+
+void target_kill_use(entity this, entity actor, entity trigger)
+{
+       if(actor.takedamage == DAMAGE_NO)
+               return;
+
+       if(!actor.iscreature && !actor.damagedbytriggers)
+               return;
+
+       Damage(actor, this, trigger, 1000, DEATH_HURTTRIGGER.m_id, DMG_NOWEP, actor.origin, '0 0 0');
+}
+
+spawnfunc(target_kill)
+{
+    this.classname = "target_kill";
+
+    if (this.message == "")
+               this.message = "was in the wrong place";
+
+    this.use = target_kill_use;
+}
+
+#endif
diff --git a/qcsrc/common/mapobjects/target/kill.qh b/qcsrc/common/mapobjects/target/kill.qh
new file mode 100644 (file)
index 0000000..6f70f09
--- /dev/null
@@ -0,0 +1 @@
+#pragma once
diff --git a/qcsrc/common/mapobjects/target/levelwarp.qc b/qcsrc/common/mapobjects/target/levelwarp.qc
new file mode 100644 (file)
index 0000000..21419cf
--- /dev/null
@@ -0,0 +1,21 @@
+#include "levelwarp.qh"
+
+#ifdef SVQC
+void target_levelwarp_use(entity this, entity actor, entity trigger)
+{
+       if(!autocvar_g_campaign)
+               return; // only in campaign
+
+       if(this.cnt)
+               CampaignLevelWarp(this.cnt - 1); // specific level
+       else
+               CampaignLevelWarp(-1); // next level
+}
+
+spawnfunc(target_levelwarp)
+{
+       // this.cnt is index (starting from 1) of the campaign level to warp to
+       // 0 means next level
+       this.use = target_levelwarp_use;
+}
+#endif
diff --git a/qcsrc/common/mapobjects/target/levelwarp.qh b/qcsrc/common/mapobjects/target/levelwarp.qh
new file mode 100644 (file)
index 0000000..6f70f09
--- /dev/null
@@ -0,0 +1 @@
+#pragma once
diff --git a/qcsrc/common/mapobjects/target/location.qc b/qcsrc/common/mapobjects/target/location.qc
new file mode 100644 (file)
index 0000000..5774f45
--- /dev/null
@@ -0,0 +1,25 @@
+#include "location.qh"
+#ifdef SVQC
+void target_push_init(entity this);
+
+spawnfunc(target_location)
+{
+    this.classname = "target_location";
+    // location name in netname
+    // eventually support: count, teamgame selectors, line of sight?
+
+    target_push_init(this);
+
+    IL_PUSH(g_locations, this);
+}
+
+spawnfunc(info_location)
+{
+    this.classname = "target_location";
+    this.message = this.netname;
+
+    target_push_init(this);
+
+    IL_PUSH(g_locations, this);
+}
+#endif
diff --git a/qcsrc/common/mapobjects/target/location.qh b/qcsrc/common/mapobjects/target/location.qh
new file mode 100644 (file)
index 0000000..6f70f09
--- /dev/null
@@ -0,0 +1 @@
+#pragma once
diff --git a/qcsrc/common/mapobjects/target/music.qc b/qcsrc/common/mapobjects/target/music.qc
new file mode 100644 (file)
index 0000000..5a63872
--- /dev/null
@@ -0,0 +1,344 @@
+#include "music.qh"
+#if defined(CSQC)
+#elif defined(MENUQC)
+#elif defined(SVQC)
+    #include <common/constants.qh>
+    #include <common/net_linked.qh>
+    #include <server/constants.qh>
+    #include <server/defs.qh>
+#endif
+
+REGISTER_NET_TEMP(TE_CSQC_TARGET_MUSIC)
+REGISTER_NET_LINKED(ENT_CLIENT_TRIGGER_MUSIC)
+
+#ifdef SVQC
+
+IntrusiveList g_targetmusic_list;
+STATIC_INIT(g_targetmusic_list)
+{
+       g_targetmusic_list = IL_NEW();
+}
+
+// values:
+//   volume
+//   noise
+//   targetname
+//   lifetime
+//   fade_time
+//   fade_rate
+// when triggered, the music is overridden for activator until lifetime (or forever, if lifetime is 0)
+// when targetname is not set, THIS ONE is default
+void target_music_sendto(entity this, int to, bool is)
+{
+       WriteHeader(to, TE_CSQC_TARGET_MUSIC);
+       WriteShort(to, etof(this));
+       WriteByte(to, this.volume * 255.0 * is);
+       WriteByte(to, this.fade_time * 16.0);
+       WriteByte(to, this.fade_rate * 16.0);
+       WriteByte(to, this.lifetime);
+       WriteString(to, this.noise);
+}
+void target_music_reset(entity this)
+{
+       if (this.targetname == "")
+       {
+               target_music_sendto(this, MSG_ALL, true);
+       }
+}
+void target_music_kill()
+{
+       IL_EACH(g_targetmusic_list, true,
+       {
+               it.volume = 0;
+        if (it.targetname == "")
+            target_music_sendto(it, MSG_ALL, true);
+        else
+            target_music_sendto(it, MSG_ALL, false);
+       });
+}
+void target_music_use(entity this, entity actor, entity trigger)
+{
+       if(!actor)
+               return;
+       if(IS_REAL_CLIENT(actor))
+       {
+               msg_entity = actor;
+               target_music_sendto(this, MSG_ONE, true);
+       }
+       FOREACH_CLIENT(IS_SPEC(it) && it.enemy == actor, {
+               msg_entity = it;
+               target_music_sendto(this, MSG_ONE, true);
+       });
+}
+spawnfunc(target_music)
+{
+       this.use = target_music_use;
+       this.reset = target_music_reset;
+       if(!this.volume)
+               this.volume = 1;
+       IL_PUSH(g_targetmusic_list, this);
+       if(this.targetname == "")
+               target_music_sendto(this, MSG_INIT, true);
+       else
+               target_music_sendto(this, MSG_INIT, false);
+}
+void TargetMusic_RestoreGame()
+{
+       IL_EACH(g_targetmusic_list, true,
+       {
+               if(it.targetname == "")
+                       target_music_sendto(it, MSG_INIT, true);
+               else
+                       target_music_sendto(it, MSG_INIT, false);
+       });
+}
+// values:
+//   volume
+//   noise
+//   targetname
+//   fade_time
+// spawnflags:
+//   START_DISABLED
+// can be disabled/enabled for everyone with relays
+bool trigger_music_SendEntity(entity this, entity to, int sendflags)
+{
+       WriteHeader(MSG_ENTITY, ENT_CLIENT_TRIGGER_MUSIC);
+       WriteByte(MSG_ENTITY, sendflags);
+       if(sendflags & SF_MUSIC_ORIGIN)
+       {
+               WriteVector(MSG_ENTITY, this.origin);
+       }
+       if(sendflags & SF_TRIGGER_INIT)
+       {
+               if(this.model != "null")
+               {
+                       WriteShort(MSG_ENTITY, this.modelindex);
+                       WriteVector(MSG_ENTITY, this.mins);
+                       WriteVector(MSG_ENTITY, this.maxs);
+               }
+               else
+               {
+                       WriteShort(MSG_ENTITY, 0);
+                       WriteVector(MSG_ENTITY, this.maxs);
+               }
+               WriteByte(MSG_ENTITY, this.volume * 255.0);
+               WriteByte(MSG_ENTITY, this.fade_time * 16.0);
+               WriteByte(MSG_ENTITY, this.fade_rate * 16.0);
+               WriteString(MSG_ENTITY, this.noise);
+       }
+       if(sendflags & SF_TRIGGER_UPDATE)
+       {
+               WriteByte(MSG_ENTITY, this.active);
+       }
+       return true;
+}
+void trigger_music_reset(entity this)
+{
+       if(this.spawnflags & START_DISABLED)
+       {
+               this.setactive(this, ACTIVE_NOT);
+       }
+       else
+       {
+               this.setactive(this, ACTIVE_ACTIVE);
+       }
+}
+
+spawnfunc(trigger_music)
+{
+       if(this.model != "")
+       {
+               _setmodel(this, this.model);
+       }
+       if(!this.volume)
+       {
+               this.volume = 1;
+       }
+       if(!this.modelindex)
+       {
+               setorigin(this, this.origin + this.mins);
+               setsize(this, '0 0 0', this.maxs - this.mins);
+       }
+
+       this.setactive = generic_netlinked_setactive;
+       this.use = generic_netlinked_legacy_use; // backwards compatibility
+       this.reset = trigger_music_reset;
+       this.reset(this);
+
+       Net_LinkEntity(this, false, 0, trigger_music_SendEntity);
+}
+#elif defined(CSQC)
+
+entity TargetMusic_list;
+STATIC_INIT(TargetMusic_list)
+{
+       TargetMusic_list = LL_NEW();
+}
+
+void TargetMusic_Advance()
+{
+       // run AFTER all the thinks!
+       entity best = music_default;
+       if (music_target && time < music_target.lifetime)
+       {
+               best = music_target;
+       }
+       if (music_trigger)
+       {
+               best = music_trigger;
+       }
+       LL_EACH(TargetMusic_list, it.noise, {
+               const float vol0 = (getsoundtime(it, CH_BGM_SINGLE) >= 0) ? it.lastvol : -1;
+               if (it == best)
+               {
+                       // increase volume
+                       it.state = (it.fade_time > 0) ? bound(0, it.state + frametime / it.fade_time, 1) : 1;
+               }
+               else
+               {
+                       // decrease volume
+                       it.state = (it.fade_rate > 0) ? bound(0, it.state - frametime / it.fade_rate, 1) : 0;
+               }
+               const float vol = it.state * it.volume * autocvar_bgmvolume;
+               if (vol != vol0)
+               {
+                       if(vol0 < 0)
+                               sound7(it, CH_BGM_SINGLE, it.noise, vol, ATTEN_NONE, 0, BIT(4)); // restart
+                       else
+                               sound7(it, CH_BGM_SINGLE, "", vol, ATTEN_NONE, 0, BIT(4));
+                       it.lastvol = vol;
+               }
+       });
+       music_trigger = NULL;
+       bgmtime = (best) ? getsoundtime(best, CH_BGM_SINGLE) : gettime(GETTIME_CDTRACK);
+}
+
+NET_HANDLE(TE_CSQC_TARGET_MUSIC, bool isNew)
+{
+       Net_TargetMusic();
+       return true;
+}
+
+void Net_TargetMusic()
+{
+       const int id = ReadShort();
+       const float vol = ReadByte() / 255.0;
+       const float fai = ReadByte() / 16.0;
+       const float fao = ReadByte() / 16.0;
+       const float tim = ReadByte();
+       const string noi = ReadString();
+
+       entity e = NULL;
+       LL_EACH(TargetMusic_list, it.count == id, { e = it; break; });
+       if (!e)
+       {
+               LL_PUSH(TargetMusic_list, e = new_pure(TargetMusic));
+               e.count = id;
+       }
+       if(e.noise != noi)
+       {
+               strcpy(e.noise, noi);
+               precache_sound(e.noise);
+               _sound(e, CH_BGM_SINGLE, e.noise, 0, ATTEN_NONE);
+               if(getsoundtime(e, CH_BGM_SINGLE) < 0)
+               {
+                       LOG_TRACEF("Cannot initialize sound %s", e.noise);
+                       strfree(e.noise);
+               }
+       }
+       e.volume = vol;
+       e.fade_time = fai;
+       e.fade_rate = fao;
+       if(vol > 0)
+       {
+               if(tim == 0)
+               {
+                       music_default = e;
+                       if(!music_disabled)
+                       {
+                               e.state = 2;
+                               cvar_settemp("music_playlist_index", "-1"); // don't use playlists
+                               localcmd("cd stop\n"); // just in case
+                               music_disabled = 1;
+                       }
+               }
+               else
+               {
+                       music_target = e;
+                       e.lifetime = time + tim;
+               }
+       }
+}
+
+void Ent_TriggerMusic_Think(entity this)
+{
+       if(this.active == ACTIVE_NOT)
+       {
+               return;
+       }
+       vector org = (csqcplayer) ? csqcplayer.origin : view_origin;
+       if(WarpZoneLib_BoxTouchesBrush(org + STAT(PL_MIN), org + STAT(PL_MAX), this, NULL))
+       {
+               music_trigger = this;
+       }
+}
+
+void Ent_TriggerMusic_Remove(entity this)
+{
+    strfree(this.noise);
+}
+
+NET_HANDLE(ENT_CLIENT_TRIGGER_MUSIC, bool isnew)
+{
+       int sendflags = ReadByte();
+       if(sendflags & SF_MUSIC_ORIGIN)
+       {
+               this.origin = ReadVector();
+       }
+       if(sendflags & SF_TRIGGER_INIT)
+       {
+               this.modelindex = ReadShort();
+               if(this.modelindex)
+               {
+                       this.mins = ReadVector();
+                       this.maxs = ReadVector();
+               }
+               else
+               {
+                       this.mins    = '0 0 0';
+                       this.maxs = ReadVector();
+               }
+
+               this.volume = ReadByte() / 255.0;
+               this.fade_time = ReadByte() / 16.0;
+               this.fade_rate = ReadByte() / 16.0;
+               string s = this.noise;
+               strcpy(this.noise, ReadString());
+               if(this.noise != s)
+               {
+                       precache_sound(this.noise);
+                       sound7(this, CH_BGM_SINGLE, this.noise, 0, ATTEN_NONE, 0, BIT(4));
+                       if(getsoundtime(this, CH_BGM_SINGLE) < 0)
+                       {
+                               LOG_WARNF("Cannot initialize sound %s", this.noise);
+                               strfree(this.noise);
+                       }
+               }
+       }
+       if(sendflags & SF_TRIGGER_UPDATE)
+       {
+               this.active = ReadByte();
+       }
+
+       setorigin(this, this.origin);
+       setsize(this, this.mins, this.maxs);
+       this.draw = Ent_TriggerMusic_Think;
+       if(isnew)
+       {
+               LL_PUSH(TargetMusic_list, this);
+               IL_PUSH(g_drawables, this);
+       }
+       return true;
+}
+
+#endif
diff --git a/qcsrc/common/mapobjects/target/music.qh b/qcsrc/common/mapobjects/target/music.qh
new file mode 100644 (file)
index 0000000..ccf3f67
--- /dev/null
@@ -0,0 +1,28 @@
+#pragma once
+
+.float lifetime;
+
+const int SF_MUSIC_ORIGIN = BIT(2);
+
+#ifdef CSQC
+float music_disabled;
+entity music_default;
+entity music_target;
+entity music_trigger;
+// FIXME also control bgmvolume here, to not require a target_music for the default track.
+
+entityclass(TargetMusic);
+classfield(TargetMusic) .int state;
+classfield(TargetMusic) .float lastvol;
+
+void TargetMusic_Advance();
+
+void Net_TargetMusic();
+
+void Ent_TriggerMusic_Think(entity this);
+
+void Ent_TriggerMusic_Remove(entity this);
+
+#elif defined(SVQC)
+void target_music_kill();
+#endif
diff --git a/qcsrc/common/mapobjects/target/spawn.qc b/qcsrc/common/mapobjects/target/spawn.qc
new file mode 100644 (file)
index 0000000..9c999ed
--- /dev/null
@@ -0,0 +1,340 @@
+#include "spawn.qh"
+#if defined(CSQC)
+#elif defined(MENUQC)
+#elif defined(SVQC)
+    #include <common/util.qh>
+    #include <server/defs.qh>
+#endif
+
+#ifdef SVQC
+
+// spawner entity
+// "classname" "target_spawn"
+// "message" "fieldname value fieldname value ..."
+// "spawnflags"
+//   ON_MAPLOAD = trigger on map load
+
+float target_spawn_initialized;
+.void(entity this) target_spawn_spawnfunc;
+float target_spawn_spawnfunc_field;
+.entity target_spawn_activator;
+.float target_spawn_id;
+float target_spawn_count;
+
+void target_spawn_helper_setmodel(entity this)
+{
+       _setmodel(this, this.model);
+}
+
+void target_spawn_helper_setsize(entity this)
+{
+       setsize(this, this.mins, this.maxs);
+}
+
+void target_spawn_edit_entity(entity this, entity e, string msg, entity kt, entity t2, entity t3, entity t4, entity act, entity trigger)
+{
+       float i, n, valuefieldpos;
+       string key, value, valuefield, valueoffset, valueoffsetrandom;
+       entity valueent;
+       vector data, data2;
+
+       n = tokenize_console(msg);
+
+       for(i = 0; i < n-1; i += 2)
+       {
+               key = argv(i);
+               value = argv(i+1);
+               if(key == "$")
+               {
+                       data.x = -1;
+                       data.y = FIELD_STRING;
+               }
+               else
+               {
+                       data = stov(db_get(TemporaryDB, strcat("/target_spawn/field/", key)));
+                       if(data.y == 0) // undefined field, i.e., invalid type
+                       {
+                               LOG_INFO("target_spawn: invalid/unknown entity key ", key, " specified, ignored!");
+                               continue;
+                       }
+               }
+               if(substring(value, 0, 1) == "$")
+               {
+                       value = substring(value, 1, strlen(value) - 1);
+                       if(substring(value, 0, 1) == "$")
+                       {
+                               // deferred replacement
+                               // do nothing
+                               // useful for creating target_spawns with this!
+                       }
+                       else
+                       {
+                               // replace me!
+                               valuefieldpos = strstrofs(value, "+", 0);
+                               valueoffset = "";
+                               if(valuefieldpos != -1)
+                               {
+                                       valueoffset = substring(value, valuefieldpos + 1, strlen(value) - valuefieldpos - 1);
+                                       value = substring(value, 0, valuefieldpos);
+                               }
+
+                               valuefieldpos = strstrofs(valueoffset, "+", 0);
+                               valueoffsetrandom = "";
+                               if(valuefieldpos != -1)
+                               {
+                                       valueoffsetrandom = substring(valueoffset, valuefieldpos + 1, strlen(valueoffset) - valuefieldpos - 1);
+                                       valueoffset = substring(valueoffset, 0, valuefieldpos);
+                               }
+
+                               valuefieldpos = strstrofs(value, ".", 0);
+                               valuefield = "";
+                               if(valuefieldpos != -1)
+                               {
+                                       valuefield = substring(value, valuefieldpos + 1, strlen(value) - valuefieldpos - 1);
+                                       value = substring(value, 0, valuefieldpos);
+                               }
+
+                               if(value == "self")
+                               {
+                                       valueent = this;
+                                       value = "";
+                               }
+                               else if(value == "activator")
+                               {
+                                       valueent = act;
+                                       value = "";
+                               }
+                               else if(value == "other")
+                               {
+                                       valueent = trigger;
+                                       value = "";
+                               }
+                               else if(value == "pusher")
+                               {
+                                       if(time < act.pushltime)
+                                               valueent = act.pusher;
+                                       else
+                                               valueent = NULL;
+                                       value = "";
+                               }
+                               else if(value == "target")
+                               {
+                                       valueent = e;
+                                       value = "";
+                               }
+                               else if(value == "killtarget")
+                               {
+                                       valueent = kt;
+                                       value = "";
+                               }
+                               else if(value == "target2")
+                               {
+                                       valueent = t2;
+                                       value = "";
+                               }
+                               else if(value == "target3")
+                               {
+                                       valueent = t3;
+                                       value = "";
+                               }
+                               else if(value == "target4")
+                               {
+                                       valueent = t4;
+                                       value = "";
+                               }
+                               else if(value == "time")
+                               {
+                                       valueent = NULL;
+                                       value = ftos(time);
+                               }
+                               else
+                               {
+                                       LOG_INFO("target_spawn: invalid/unknown variable replacement ", value, " specified, ignored!");
+                                       continue;
+                               }
+
+                               if(valuefield == "")
+                               {
+                                       if(value == "")
+                                               value = ftos(etof(valueent));
+                               }
+                               else
+                               {
+                                       if(value != "")
+                                       {
+                                               LOG_INFO("target_spawn: try to get a field of a non-entity, ignored!");
+                                               continue;
+                                       }
+                                       data2 = stov(db_get(TemporaryDB, strcat("/target_spawn/field/", valuefield)));
+                                       if(data2_y == 0) // undefined field, i.e., invalid type
+                                       {
+                                               LOG_INFO("target_spawn: invalid/unknown entity key replacement ", valuefield, " specified, ignored!");
+                                               continue;
+                                       }
+                                       value = getentityfieldstring(data2_x, valueent);
+                               }
+
+                               if(valueoffset != "")
+                               {
+                                       switch(data.y)
+                                       {
+                                               case FIELD_STRING:
+                                                       value = strcat(value, valueoffset);
+                                                       break;
+                                               case FIELD_FLOAT:
+                                                       value = ftos(stof(value) + stof(valueoffset));
+                                                       break;
+                                               case FIELD_VECTOR:
+                                                       value = vtos(stov(value) + stov(valueoffset));
+                                                       break;
+                                               default:
+                                                       LOG_INFO("target_spawn: only string, float and vector fields can do calculations, calculation ignored!");
+                                                       break;
+                                       }
+                               }
+
+                               if(valueoffsetrandom != "")
+                               {
+                                       switch(data.y)
+                                       {
+                                               case FIELD_FLOAT:
+                                                       value = ftos(stof(value) + random() * stof(valueoffsetrandom));
+                                                       break;
+                                               case FIELD_VECTOR:
+                                                       data2 = stov(valueoffsetrandom);
+                                                       value = vtos(stov(value) + random() * data2_x * '1 0 0' + random() * data2_y * '0 1 0' + random() * data2_z * '0 0 1');
+                                                       break;
+                                               default:
+                                                       LOG_INFO("target_spawn: only float and vector fields can do random calculations, calculation ignored!");
+                                                       break;
+                                       }
+                               }
+                       }
+               }
+               if(key == "$")
+               {
+                       if(substring(value, 0, 1) == "_")
+                               value = strcat("target_spawn_helper", value);
+                       putentityfieldstring(target_spawn_spawnfunc_field, e, value);
+
+                       e.target_spawn_spawnfunc(e);
+
+                       // We called an external function, so we have to re-tokenize msg.
+                       n = tokenize_console(msg);
+               }
+               else
+               {
+                       if(data.y == FIELD_VECTOR)
+                               value = strreplace("'", "", value); // why?!?
+                       putentityfieldstring(data.x, e, value);
+               }
+       }
+}
+
+void target_spawn_useon(entity e, entity this, entity actor, entity trigger)
+{
+       this.target_spawn_activator = actor;
+       target_spawn_edit_entity(
+               this,
+               e,
+               this.message,
+               find(NULL, targetname, this.killtarget),
+               find(NULL, targetname, this.target2),
+               find(NULL, targetname, this.target3),
+               find(NULL, targetname, this.target4),
+               actor,
+               trigger
+       );
+}
+
+bool target_spawn_cancreate(entity this)
+{
+       float c;
+       entity e;
+
+       c = this.count;
+       if(c == 0) // no limit?
+               return true;
+
+       ++c; // increase count to not include MYSELF
+       for(e = NULL; (e = findfloat(e, target_spawn_id, this.target_spawn_id)); --c)
+               ;
+
+       // if c now is 0, we have AT LEAST the given count (maybe more), so don't spawn any more
+       if(c == 0)
+               return false;
+       return true;
+}
+
+void target_spawn_use(entity this, entity actor, entity trigger)
+{
+       if(this.target == "")
+       {
+               // spawn new entity
+               if(!target_spawn_cancreate(this))
+                       return;
+               entity e = spawn();
+               e.spawnfunc_checked = true;
+               target_spawn_useon(e, this, actor, trigger);
+               e.target_spawn_id = this.target_spawn_id;
+       }
+       else if(this.target == "*activator")
+       {
+               // edit entity
+               if(actor)
+                       target_spawn_useon(actor, this, actor, trigger);
+       }
+       else
+       {
+               // edit entity
+               FOREACH_ENTITY_STRING(targetname, this.target,
+               {
+                       target_spawn_useon(it, this, actor, trigger);
+               });
+       }
+}
+
+void target_spawn_spawnfirst(entity this)
+{
+       entity act = this.target_spawn_activator;
+       if(this.spawnflags & ON_MAPLOAD)
+               target_spawn_use(this, act, NULL);
+}
+
+void initialize_field_db()
+{
+       if(!target_spawn_initialized)
+       {
+               float n, i;
+               string fn;
+               vector prev, next;
+               float ft;
+
+               n = numentityfields();
+               for(i = 0; i < n; ++i)
+               {
+                       fn = entityfieldname(i);
+                       ft = entityfieldtype(i);
+                       next = i * '1 0 0' + ft * '0 1 0' + '0 0 1';
+                       prev = stov(db_get(TemporaryDB, strcat("/target_spawn/field/", fn)));
+                       if(prev.y == 0)
+                       {
+                               db_put(TemporaryDB, strcat("/target_spawn/field/", fn), vtos(next));
+                               if(fn == "target_spawn_spawnfunc")
+                                       target_spawn_spawnfunc_field = i;
+                       }
+               }
+
+               target_spawn_initialized = 1;
+       }
+}
+
+spawnfunc(target_spawn)
+{
+       initialize_field_db();
+       this.use = target_spawn_use;
+       this.message = strzone(strreplace("'", "\"", this.message));
+       this.target_spawn_id = ++target_spawn_count;
+       InitializeEntity(this, target_spawn_spawnfirst, INITPRIO_LAST);
+}
+#endif
diff --git a/qcsrc/common/mapobjects/target/spawn.qh b/qcsrc/common/mapobjects/target/spawn.qh
new file mode 100644 (file)
index 0000000..6f70f09
--- /dev/null
@@ -0,0 +1 @@
+#pragma once
diff --git a/qcsrc/common/mapobjects/target/spawnpoint.qc b/qcsrc/common/mapobjects/target/spawnpoint.qc
new file mode 100644 (file)
index 0000000..fe15385
--- /dev/null
@@ -0,0 +1,24 @@
+#include "spawnpoint.qh"
+
+#ifdef SVQC
+void target_spawnpoint_use(entity this, entity actor, entity trigger)
+{
+       if(this.active != ACTIVE_ACTIVE)
+               return;
+
+       actor.spawnpoint_targ = this;
+}
+
+void target_spawnpoint_reset(entity this)
+{
+       this.active = ACTIVE_ACTIVE;
+}
+
+// TODO: persistent spawnflag?
+spawnfunc(target_spawnpoint)
+{
+       this.active = ACTIVE_ACTIVE;
+       this.use = target_spawnpoint_use;
+       this.reset = target_spawnpoint_reset;
+}
+#endif
diff --git a/qcsrc/common/mapobjects/target/spawnpoint.qh b/qcsrc/common/mapobjects/target/spawnpoint.qh
new file mode 100644 (file)
index 0000000..2eeb8da
--- /dev/null
@@ -0,0 +1,5 @@
+#pragma once
+
+#ifdef SVQC
+.entity spawnpoint_targ;
+#endif
diff --git a/qcsrc/common/mapobjects/target/speaker.qc b/qcsrc/common/mapobjects/target/speaker.qc
new file mode 100644 (file)
index 0000000..11c9ad7
--- /dev/null
@@ -0,0 +1,138 @@
+#include "speaker.qh"
+#ifdef SVQC
+// TODO add a way to do looped sounds with sound(); then complete this entity
+void target_speaker_use_off(entity this, entity actor, entity trigger);
+void target_speaker_use_activator(entity this, entity actor, entity trigger)
+{
+       if (!IS_REAL_CLIENT(actor))
+               return;
+       string snd;
+       if(substring(this.noise, 0, 1) == "*")
+       {
+               var .string sample = GetVoiceMessageSampleField(substring(this.noise, 1, -1));
+               if(GetPlayerSoundSampleField_notFound)
+                       snd = SND(Null);
+               else if(actor.(sample) == "")
+                       snd = SND(Null);
+               else
+               {
+                       tokenize_console(actor.(sample));
+                       float n;
+                       n = stof(argv(1));
+                       if(n > 0)
+                               snd = strcat(argv(0), ftos(floor(random() * n + 1)), ".wav"); // randomization
+                       else
+                               snd = strcat(argv(0), ".wav"); // randomization
+               }
+       }
+       else
+               snd = this.noise;
+       msg_entity = actor;
+       soundto(MSG_ONE, this, CH_TRIGGER, snd, VOL_BASE * this.volume, this.atten);
+}
+void target_speaker_use_on(entity this, entity actor, entity trigger)
+{
+       string snd;
+       if(substring(this.noise, 0, 1) == "*")
+       {
+               var .string sample = GetVoiceMessageSampleField(substring(this.noise, 1, -1));
+               if(GetPlayerSoundSampleField_notFound)
+                       snd = SND(Null);
+               else if(actor.(sample) == "")
+                       snd = SND(Null);
+               else
+               {
+                       tokenize_console(actor.(sample));
+                       float n;
+                       n = stof(argv(1));
+                       if(n > 0)
+                               snd = strcat(argv(0), ftos(floor(random() * n + 1)), ".wav"); // randomization
+                       else
+                               snd = strcat(argv(0), ".wav"); // randomization
+               }
+       }
+       else
+               snd = this.noise;
+       _sound(this, CH_TRIGGER_SINGLE, snd, VOL_BASE * this.volume, this.atten);
+       if(this.spawnflags & (SPEAKER_LOOPED_ON + SPEAKER_LOOPED_OFF))
+               this.use = target_speaker_use_off;
+}
+void target_speaker_use_off(entity this, entity actor, entity trigger)
+{
+       sound(this, CH_TRIGGER_SINGLE, SND_Null, VOL_BASE * this.volume, this.atten);
+       this.use = target_speaker_use_on;
+}
+void target_speaker_reset(entity this)
+{
+       if(this.spawnflags & SPEAKER_LOOPED_ON)
+       {
+               if(this.use == target_speaker_use_on)
+                       target_speaker_use_on(this, NULL, NULL);
+       }
+       else if(this.spawnflags & SPEAKER_LOOPED_OFF)
+       {
+               if(this.use == target_speaker_use_off)
+                       target_speaker_use_off(this, NULL, NULL);
+       }
+}
+
+spawnfunc(target_speaker)
+{
+       // TODO: "*" prefix to sound file name
+       // TODO: wait and random (just, HOW? random is not a field)
+       if(this.noise)
+               precache_sound (this.noise);
+
+       if(!this.atten && (this.spawnflags & SPEAKER_GLOBAL))
+       {
+               LOG_WARN("target_speaker uses legacy spawnflag GLOBAL (BIT(2)), please set atten to -1 instead");
+               this.atten = -1;
+       }
+
+       if(!this.atten)
+       {
+               IFTARGETED
+                       this.atten = ATTEN_NORM;
+               else
+                       this.atten = ATTEN_STATIC;
+       }
+       else if(this.atten < 0)
+               this.atten = 0;
+
+       if(!this.volume)
+               this.volume = 1;
+
+       IFTARGETED
+       {
+               if(this.spawnflags & SPEAKER_ACTIVATOR)
+                       this.use = target_speaker_use_activator;
+               else if(this.spawnflags & SPEAKER_LOOPED_ON)
+               {
+                       target_speaker_use_on(this, NULL, NULL);
+                       this.reset = target_speaker_reset;
+               }
+               else if(this.spawnflags & SPEAKER_LOOPED_OFF)
+               {
+                       this.use = target_speaker_use_on;
+                       this.reset = target_speaker_reset;
+               }
+               else
+                       this.use = target_speaker_use_on;
+       }
+       else if(this.spawnflags & SPEAKER_LOOPED_ON)
+       {
+               ambientsound (this.origin, this.noise, VOL_BASE * this.volume, this.atten);
+               delete(this);
+       }
+       else if(this.spawnflags & SPEAKER_LOOPED_OFF)
+       {
+               objerror(this, "This sound entity can never be activated");
+       }
+       else
+       {
+               // Quake/Nexuiz fallback
+               ambientsound (this.origin, this.noise, VOL_BASE * this.volume, this.atten);
+               delete(this);
+       }
+}
+#endif
diff --git a/qcsrc/common/mapobjects/target/speaker.qh b/qcsrc/common/mapobjects/target/speaker.qh
new file mode 100644 (file)
index 0000000..53e0194
--- /dev/null
@@ -0,0 +1,7 @@
+#pragma once
+
+
+const int SPEAKER_LOOPED_ON = BIT(0);
+const int SPEAKER_LOOPED_OFF = BIT(1);
+const int SPEAKER_GLOBAL = BIT(2); // legacy, set speaker atten to -1 instead
+const int SPEAKER_ACTIVATOR = BIT(3);
diff --git a/qcsrc/common/mapobjects/target/voicescript.qc b/qcsrc/common/mapobjects/target/voicescript.qc
new file mode 100644 (file)
index 0000000..