]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/commitdiff
Merge branch 'master' into mirceakitsune/sandbox
authorMircea Kitsune <sonichedgehog_hyperblast00@yahoo.com>
Tue, 15 Nov 2011 11:03:26 +0000 (13:03 +0200)
committerMircea Kitsune <sonichedgehog_hyperblast00@yahoo.com>
Tue, 15 Nov 2011 11:03:26 +0000 (13:03 +0200)
41 files changed:
defaultXonotic.cfg
effectinfo.txt
gfx/menu/luminos/skinvalues.txt
gfx/menu/wickedx/skinvalues.txt
gfx/menu/xaw/skinvalues.txt
keybinds.txt
qcsrc/menu/classes.c
qcsrc/menu/skin-customizables.inc
qcsrc/menu/xonotic/dialog_sandboxtools.c [new file with mode: 0644]
qcsrc/menu/xonotic/mainwindow.c
qcsrc/server/autocvars.qh
qcsrc/server/cheats.qc
qcsrc/server/cheats.qh
qcsrc/server/defs.qh
qcsrc/server/miscfunctions.qc
qcsrc/server/mutators/base.qh
qcsrc/server/mutators/mutators.qh
qcsrc/server/mutators/sandbox.qc [new file with mode: 0644]
qcsrc/server/progs.src
qcsrc/server/sv_main.qc
sound/object/impact_flesh_1.ogg [new file with mode: 0644]
sound/object/impact_flesh_2.ogg [new file with mode: 0644]
sound/object/impact_flesh_3.ogg [new file with mode: 0644]
sound/object/impact_flesh_4.ogg [new file with mode: 0644]
sound/object/impact_flesh_5.ogg [new file with mode: 0644]
sound/object/impact_metal_1.ogg [new file with mode: 0644]
sound/object/impact_metal_2.ogg [new file with mode: 0644]
sound/object/impact_metal_3.ogg [new file with mode: 0644]
sound/object/impact_metal_4.ogg [new file with mode: 0644]
sound/object/impact_metal_5.ogg [new file with mode: 0644]
sound/object/impact_stone_1.ogg [new file with mode: 0644]
sound/object/impact_stone_2.ogg [new file with mode: 0644]
sound/object/impact_stone_3.ogg [new file with mode: 0644]
sound/object/impact_stone_4.ogg [new file with mode: 0644]
sound/object/impact_stone_5.ogg [new file with mode: 0644]
sound/object/impact_wood_1.ogg [new file with mode: 0644]
sound/object/impact_wood_2.ogg [new file with mode: 0644]
sound/object/impact_wood_3.ogg [new file with mode: 0644]
sound/object/impact_wood_4.ogg [new file with mode: 0644]
sound/object/impact_wood_5.ogg [new file with mode: 0644]
xonotic-credits.txt

index 69b97a9c8ad39f6f559c19e00e15004a83610643..c4a60c9215f7b46147cb126ecea054d98b38b8c8 100644 (file)
@@ -539,11 +539,45 @@ seta g_maplist_shuffle 1  "new randomization method: like selectrandom, but avoid
 set g_maplist_check_waypoints 0        "when 1, maps are skipped if there currently are bots, but the map has no waypoints"
 set samelevel 0 "when 1, always play the same level over and over again"
 
+set g_grab_range 200 "distance at which dragable objects can be grabbed"
+
 set g_cloaked 0 "display all players mostly invisible"
 set g_player_alpha 1
 set g_player_brightness 0      "set to 2 for brighter players"
 seta g_balance_cloaked_alpha 0.25
 
+set g_sandbox 0 "allow players to spawn and edit objects around the map"
+set g_sandbox_info 1 "print object information to the server. 1 prints info about spawned / removed objects, 2 also prints info about edited objects"
+set g_sandbox_readonly 0 "when this mode is active, players cannot modify objects or use any sandbox commands"
+set g_sandbox_storage_name default "name of the selected storage to use"
+set g_sandbox_storage_autosave 5 "storage is automatically saved every specified number of seconds"
+set g_sandbox_storage_autoload 1 "if a storage file exists for the given map, automatically load it at startup"
+set g_sandbox_editor_flood 1 "players must wait this many seconds between spawning objects"
+set g_sandbox_editor_maxobjects 1000 "maximum number of objects that may exist at a time"
+set g_sandbox_editor_free 1 "0 = players can only copy or edit their own objects, 1 = players can copy but not edit other objects, 2 = players can copy and edit all object"
+set g_sandbox_editor_distance_spawn 200 "distance at which objects spawn in front of the player"
+set g_sandbox_editor_distance_edit 300 "distance at which players can edit or remove objects they are looking at"
+set g_sandbox_object_scale_min 0.1 "minimum scale that objects can be set to"
+set g_sandbox_object_scale_max 2 "maximum scale that objects can be set to"
+set g_sandbox_object_material_velocity_min 100 "velocity objects must have while colliding for material effects to be applied"
+set g_sandbox_object_material_velocity_factor 0.002 "velocity range which decides the intensity of material effects"
+set cl_sandbox_clipboard ""
+
+seta menu_sandbox_spawn_model ""
+seta menu_sandbox_attach_bone ""
+seta menu_sandbox_edit_skin 0
+seta menu_sandbox_edit_alpha 1
+seta menu_sandbox_edit_color_main "1 1 1"
+seta menu_sandbox_edit_color_glow "1 1 1"
+seta menu_sandbox_edit_frame 0
+seta menu_sandbox_edit_scale 1
+seta menu_sandbox_edit_physics 1
+seta menu_sandbox_edit_force 1
+seta menu_sandbox_edit_material ""
+
+alias menu_showsandboxtools "menu_cmd directmenu SandboxTools"
+bind f7 menu_showsandboxtools
+
 set g_playerclip_collisions 1 "0 = disable collision testing against playerclips, might be useful on some defrag maps"
 set g_botclip_collisions 1 "0 = disable collision testing against botclips, might be useful on some defrag maps"
 
@@ -997,6 +1031,8 @@ alias togglezoom "${_togglezoom}zoom"
 
 alias reload "impulse 20"
 
+alias sandbox "cmd g_sandbox $*"
+
 // movement
 bind w +forward
 bind a +moveleft
@@ -1043,6 +1079,7 @@ bind r reload
 bind BACKSPACE dropweapon
 bind g dropweapon
 bind f +use
+bind v +button8 // drag object
 
 // misc
 bind e +hook
index 70f5fbb4c6f8fd7f03468ae16cc2e9e7bd80399d..06e153bf8973a526982e937994b836b323e46b09 100644 (file)
@@ -6407,3 +6407,109 @@ velocityoffset 0 0 200
 airfriction 4
 color 0x4F4B46 0x000000
 rotate -180 180 -20 20
+
+// metal impact effect
+// used in qcsrc/server/mutators/sandbox.qc:   pointparticles(particleeffectnum("impact_metal"), self.origin, '0 0 0', 1);
+effect impact_metal
+count 1
+type alphastatic
+tex 0 8
+size 3 6
+sizeincrease 10
+alpha 25 64 50
+gravity -0.01
+color 0x000000 0x886666
+originjitter 20 20 5
+// sparks
+effect impact_metal
+count 2
+type spark
+tex 41 41
+color 0xFFCC22 0xFF4422
+size 2 2
+alpha 255 255 112
+bounce 1.8
+stretchfactor 0.5
+velocityjitter 200 200 300
+velocitymultiplier 2
+airfriction 2
+gravity 1
+
+// stone impact effect
+// used in qcsrc/server/mutators/sandbox.qc:   pointparticles(particleeffectnum("impact_stone"), self.origin, '0 0 0', 1);
+effect impact_stone
+count 1
+type alphastatic
+tex 0 8
+size 3 6
+sizeincrease 15
+alpha 50 128 75
+gravity -0.01
+color 0x000000 0xcc9966
+originjitter 20 20 5
+// debris
+effect impact_stone
+notunderwater
+count 1
+type alphastatic
+tex 66 68
+color 0x000000 0x886644
+size 1 2
+alpha 450 750 300
+gravity 1.3
+airfriction 0.5
+bounce 1.2
+velocityjitter 124 124 324
+rotate -180 180 -1000 1000
+
+// wood impact effect
+// used in qcsrc/server/mutators/sandbox.qc:   pointparticles(particleeffectnum("impact_wood"), self.origin, '0 0 0', 1);
+effect impact_wood
+count 1
+type alphastatic
+tex 0 8
+size 3 6
+sizeincrease 10
+alpha 50 128 75
+gravity -0.01
+color 0x000000 0xcc9966
+originjitter 20 20 5
+// sparks
+effect impact_wood
+count 2
+type spark
+tex 41 41
+color 0x221100 0x221100
+size 1 8
+alpha 255 255 75
+bounce 1.5
+velocityjitter 180 180 260
+velocitymultiplier 2
+airfriction 2
+gravity 1
+
+// flesh impact effect
+// used in qcsrc/server/mutators/sandbox.qc:   pointparticles(particleeffectnum("impact_flesh"), self.origin, '0 0 0', 1);
+effect impact_flesh
+count 0.5
+type alphastatic
+tex 0 8
+size 8 12
+alpha 100 256 400
+color 0x000000 0x420000
+originjitter 11 11 11
+// blood splash
+effect impact_flesh
+count 0.3
+type blood
+tex 24 32
+size 2 6
+alpha 256 256 64
+color 0xA8FFFF 0xA8FFFFF
+bounce -1
+airfriction 1
+liquidfriction 4
+velocityjitter 96 96 96
+velocitymultiplier 5
+staincolor 0x808080 0x808080
+staintex 16 24
index bf147b4f4821636d4a4d7ed1136fe81ed1fdc363..b4102234acd4e8bc06eef8253bfcc90d83538333 100755 (executable)
@@ -166,6 +166,7 @@ COLOR_CHECKBOX_D                '1 1 1'
 COLOR_DIALOG_MULTIPLAYER        '1 1 1'
 COLOR_DIALOG_SETTINGS           '1 1 1'
 COLOR_DIALOG_TEAMSELECT         '1 1 1'
+COLOR_DIALOG_SANDBOXTOOLS       '1 1 1'
 COLOR_DIALOG_QUIT               '1 1 1'
 COLOR_DIALOG_ADVANCED           '1 1 1'
 COLOR_DIALOG_MUTATORS           '1 1 1'
index 2b09e5624ccf0ad14e2f77e387911543e36bb3ec..d6b7197a1d32c099a1ffbff4fdee06210e066455 100755 (executable)
@@ -166,6 +166,7 @@ COLOR_CHECKBOX_D                '1 1 1'
 COLOR_DIALOG_MULTIPLAYER        '1 1 1'
 COLOR_DIALOG_SETTINGS           '1 1 1'
 COLOR_DIALOG_TEAMSELECT         '1 1 1'
+COLOR_DIALOG_SANDBOXTOOLS       '1 1 1'
 COLOR_DIALOG_QUIT               '1 1 1'
 COLOR_DIALOG_ADVANCED           '1 1 1'
 COLOR_DIALOG_MUTATORS           '1 1 1'
index 376f1596c918ccf64790748f4e71626857ce53da..5afcb09cfe3751cd5efedde9758be73cd512e864 100644 (file)
@@ -21,6 +21,7 @@ AVOID_TOOLTIP                   '8 8 0'
 COLOR_DIALOG_MULTIPLAYER        '1 1 1'
 COLOR_DIALOG_SETTINGS           '1 1 1'
 COLOR_DIALOG_TEAMSELECT         '1 1 1'
+COLOR_DIALOG_SANDBOXTOOLS       '1 1 1'
 COLOR_DIALOG_QUIT               '1 0 0'
 COLOR_DIALOG_ADVANCED           '1 1 1'
 COLOR_DIALOG_MUTATORS           '1 1 1'
index 2df89afae0e9f0ecf8b5dd4cdc6a737071a65a80..d7ec276f57f9d541057d42ebaccc63776a177069 100644 (file)
 "messagemode2"                          "team chat"
 "team_auto"                             "auto-join team"
 "menu_showteamselect"                   "team menu"
+"menu_showsandboxtools"                 "sandbox menu"
 "spec"                                  "enter spectator mode"
 "dropweapon"                            "drop weapon"
 "+use"                                  "drop key / drop flag"
+"+button8"                              "drag object"
 ""                                      ""
 ""                                      "User defined"
 "+userbind 1"                           "$userbind1"
index 5b7331e3752be37be4231150c89627e38c5baa5e..c0a9780471e3689ca0f1490c51459d9ce05c84ed 100644 (file)
@@ -29,6 +29,7 @@
 #include "xonotic/bigcommandbutton.c"
 #include "xonotic/dialog_firstrun.c"
 #include "xonotic/dialog_teamselect.c"
+#include "xonotic/dialog_sandboxtools.c"
 #include "xonotic/dialog_settings.c"
 #include "xonotic/dialog_settings_video.c"
 #include "xonotic/dialog_settings_effects.c"
index ca8f4cf8e959ee7d2a5755e74e82def02d543c0b..e8cfba3689a7bcca7ed0fe1fa1eaffa4549dd904 100644 (file)
@@ -55,6 +55,7 @@ SKINBEGIN
        SKINVECTOR(COLOR_DIALOG_MULTIPLAYER, '0.7 0.7 1');
        SKINVECTOR(COLOR_DIALOG_SETTINGS, '0.7 0.7 1');
        SKINVECTOR(COLOR_DIALOG_TEAMSELECT, '1 1 1');
+       SKINVECTOR(COLOR_DIALOG_SANDBOXTOOLS, '1 1 1');
        SKINVECTOR(COLOR_DIALOG_QUIT, '1 0 0');
        SKINVECTOR(COLOR_DIALOG_ADVANCED, '0.7 0.7 1');
        SKINVECTOR(COLOR_DIALOG_MUTATORS, '0.7 0.7 1');
diff --git a/qcsrc/menu/xonotic/dialog_sandboxtools.c b/qcsrc/menu/xonotic/dialog_sandboxtools.c
new file mode 100644 (file)
index 0000000..0c7f555
--- /dev/null
@@ -0,0 +1,91 @@
+#ifdef INTERFACE
+CLASS(XonoticSandboxToolsDialog) EXTENDS(XonoticRootDialog)
+       METHOD(XonoticSandboxToolsDialog, fill, void(entity)) // to be overridden by user to fill the dialog with controls
+       ATTRIB(XonoticSandboxToolsDialog, title, string, _("Sandbox Tools")) // ;)
+       ATTRIB(XonoticSandboxToolsDialog, color, vector, SKINCOLOR_DIALOG_SANDBOXTOOLS)
+       ATTRIB(XonoticSandboxToolsDialog, intendedWidth, float, 0.8)
+       ATTRIB(XonoticSandboxToolsDialog, rows, float, 15)
+       ATTRIB(XonoticSandboxToolsDialog, columns, float, 4)
+       ATTRIB(XonoticSandboxToolsDialog, name, string, "SandboxTools")
+ENDCLASS(XonoticSandboxToolsDialog)
+#endif
+
+#ifdef IMPLEMENTATION
+void XonoticSandboxToolsDialog_fill(entity me)
+{
+       entity e, box;
+
+       me.TR(me);
+               me.TD(me, 1, 0.25, e = makeXonoticTextLabel(0, _("Model:")));
+               me.TD(me, 1, 1.5, box = makeXonoticInputBox(1, "menu_sandbox_spawn_model"));
+                       box.forbiddenCharacters = "\r\n\\\"$"; // don't care, isn't getting saved
+                       box.maxLength = -127; // negative means encoded length in bytes
+                       box.saveImmediately = 1;
+               me.TD(me, 1, 0.5, e = makeXonoticCommandButton(_("Spawn"), '0 0 0', "sandbox object_spawn \"$menu_sandbox_spawn_model\"", 0));
+               me.TD(me, 1, 0.5, e = makeXonoticCommandButton(_("Remove *"), '0 0 0', "sandbox object_remove", 0));
+       me.TDempty(me, 0.1);
+               me.TD(me, 1, 0.5, e = makeXonoticCommandButton(_("Copy *"), '0 0 0', "sandbox object_duplicate copy cl_sandbox_clipboard", 0));
+               me.TD(me, 1, 0.5, e = makeXonoticCommandButton(_("Paste"), '0 0 0', "sandbox object_duplicate paste \"$cl_sandbox_clipboard\"", 0));
+       me.TR(me);
+               me.TD(me, 1, 0.25, e = makeXonoticTextLabel(0, _("Bone:")));
+               me.TD(me, 1, 1.5, box = makeXonoticInputBox(1, "menu_sandbox_attach_bone"));
+                       box.forbiddenCharacters = "\r\n\\\"$"; // don't care, isn't getting saved
+                       box.maxLength = -127; // negative means encoded length in bytes
+                       box.saveImmediately = 1;
+               me.TD(me, 1, 0.5, e = makeXonoticCommandButton(_("Set * as child"), '0 0 0', "sandbox object_attach get", 0));
+               me.TD(me, 1, 0.5, e = makeXonoticCommandButton(_("Attach to *"), '0 0 0', "sandbox object_attach set \"$menu_sandbox_attach_bone\"", 0));
+       me.TDempty(me, 0.1);
+               me.TD(me, 1, 0.5, e = makeXonoticCommandButton(_("Detach from *"), '0 0 0', "sandbox object_attach remove", 0));
+       me.TR(me);
+       me.TR(me);
+       me.TD(me, 1, 1.5, e = makeXonoticTextLabel(0, _("Visual object properties for *:")));
+       me.TR(me);
+               me.TD(me, 1, 0.5, e = makeXonoticCommandButton(_("Set skin:"), '0 0 0', "sandbox object_edit skin $menu_sandbox_edit_skin", 0));
+               me.TD(me, 1, 1.5, e = makeXonoticSlider(0, 99, 1, "menu_sandbox_edit_skin"));
+               me.TD(me, 1, 0.5, e = makeXonoticCommandButton(_("Set alpha:"), '0 0 0', "sandbox object_edit alpha $menu_sandbox_edit_alpha", 0));
+               me.TD(me, 1, 1.5, e = makeXonoticSlider(0.1, 1, 0.05, "menu_sandbox_edit_alpha"));
+       me.TR(me);
+               me.TD(me, 1, 0.5, e = makeXonoticCommandButton(_("Set color main:"), '0 0 0', "sandbox object_edit color_main \"$menu_sandbox_edit_color_main\"", 0));
+               me.TD(me, 2, 1.5, e = makeXonoticColorpickerString("menu_sandbox_edit_color_main", "menu_sandbox_edit_color_main"));
+               me.TD(me, 1, 0.5, e = makeXonoticCommandButton(_("Set color glow:"), '0 0 0', "sandbox object_edit color_glow \"$menu_sandbox_edit_color_glow\"", 0));
+               me.TD(me, 2, 1.5, e = makeXonoticColorpickerString("menu_sandbox_edit_color_glow", "menu_sandbox_edit_color_glow"));
+       me.TR(me);
+       me.TR(me);
+               me.TD(me, 1, 0.5, e = makeXonoticCommandButton(_("Set frame:"), '0 0 0', "sandbox object_edit frame $menu_sandbox_edit_frame", 0));
+               me.TD(me, 1, 1.5, e = makeXonoticSlider(0, 99, 1, "menu_sandbox_edit_frame"));
+       me.TR(me);
+       me.TR(me);
+       me.TD(me, 1, 1.5, e = makeXonoticTextLabel(0, _("Physical object properties for *:")));
+       me.TR(me);
+               me.TD(me, 1, 0.5, e = makeXonoticCommandButton(_("Set material:"), '0 0 0', "sandbox object_edit material \"$menu_sandbox_edit_material\"", 0));
+               me.TD(me, 1, 1.5, box = makeXonoticInputBox(1, "menu_sandbox_edit_material"));
+                       box.forbiddenCharacters = "\r\n\\\"$"; // don't care, isn't getting saved
+                       box.maxLength = -127; // negative means encoded length in bytes
+                       box.saveImmediately = 1;
+               me.TD(me, 1, 0.5, e = makeXonoticCommandButton(_("Set physics:"), '0 0 0', "sandbox object_edit physics $menu_sandbox_edit_physics", 0));
+               me.TD(me, 1, 0.5, e = makeXonoticRadioButton(1, "menu_sandbox_edit_physics", "0", _("Static")));
+               me.TD(me, 1, 0.5, e = makeXonoticRadioButton(1, "menu_sandbox_edit_physics", "1", _("Movable")));
+               me.TD(me, 1, 0.5, e = makeXonoticRadioButton(1, "menu_sandbox_edit_physics", "2", _("Physical")));
+       me.TR(me);
+               me.TD(me, 1, 0.5, e = makeXonoticCommandButton(_("Set scale:"), '0 0 0', "sandbox object_edit scale $menu_sandbox_edit_scale", 0));
+               me.TD(me, 1, 1.5, e = makeXonoticSlider(0.25, 2, 0.05, "menu_sandbox_edit_scale"));
+               me.TD(me, 1, 0.5, e = makeXonoticCommandButton(_("Set force:"), '0 0 0', "sandbox object_edit force $menu_sandbox_edit_force", 0));
+               me.TD(me, 1, 1.5, e = makeXonoticSlider(0, 10, 0.5, "menu_sandbox_edit_force"));
+       me.TR(me);
+       me.TR(me);
+               me.TD(me, 1, 0.5, e = makeXonoticCommandButton(_("Claim *"), '0 0 0', "sandbox object_claim", 0));
+               me.TDempty(me, 0.5);
+               me.TD(me, 1, 0.5, e = makeXonoticCommandButton(_("* object info"), '1 1 0.5', "sandbox object_info object; toggleconsole", 0));
+               me.TD(me, 1, 0.5, e = makeXonoticCommandButton(_("* mesh info"), '1 1 0.5', "sandbox object_info mesh; toggleconsole", 0));
+               me.TD(me, 1, 0.5, e = makeXonoticCommandButton(_("* attachment info"), '1 1 0.5', "sandbox object_info attachments; toggleconsole", 0));
+               me.TD(me, 1, 0.5, e = makeXonoticCommandButton(_("Show help"), '1 0.5 0.5', "sandbox help; toggleconsole", 0));
+               me.TD(me, 1, 1, e = makeXonoticTextLabel(0, _("* is the object you are facing")));
+
+       me.gotoRC(me, me.rows - 1, 0);
+               me.TD(me, 1, me.columns, e = makeXonoticButton(_("OK"), '0 0 0'));
+                       e.onClick = Dialog_Close;
+                       e.onClickEntity = me;
+}
+#endif
+
+/* Click. The c-word is here so you can grep for it :-) */
index 55d496047138cf23ec68b55251db14e8658d17d9..20627c7a579d25adee8be4192d7e00c8264640e5 100644 (file)
@@ -49,6 +49,10 @@ void MainWindow_configureMainWindow(entity me)
        i = spawnXonoticTeamSelectDialog();
        i.configureDialog(i);
        me.addItemCentered(me, i, i.intendedWidth * eX + i.intendedHeight * eY, SKINALPHAS_MAINMENU_z);
+
+       i = spawnXonoticSandboxToolsDialog();
+       i.configureDialog(i);
+       me.addItemCentered(me, i, i.intendedWidth * eX + i.intendedHeight * eY, SKINALPHAS_MAINMENU_z);
        
        i = spawnXonoticHUDExitDialog();
        i.configureDialog(i);
index 3b64b1b63f458cb7d2716671efdcaaeca828a785..edd3420eccb77eed2376ba46f23015e860df72c7 100644 (file)
@@ -1203,3 +1203,18 @@ float autocvar_sv_gameplayfix_gravityunaffectedbyticrate;
 float autocvar_g_trueaim_minrange;
 float autocvar_g_debug_defaultsounds;
 float autocvar_g_loituma;
+float autocvar_g_grab_range;
+float autocvar_g_sandbox_info;
+float autocvar_g_sandbox_readonly;
+string autocvar_g_sandbox_storage_name;
+float autocvar_g_sandbox_storage_autosave;
+float autocvar_g_sandbox_storage_autoload;
+float autocvar_g_sandbox_editor_flood;
+float autocvar_g_sandbox_editor_maxobjects;
+float autocvar_g_sandbox_editor_free;
+float autocvar_g_sandbox_editor_distance_spawn;
+float autocvar_g_sandbox_editor_distance_edit;
+float autocvar_g_sandbox_object_scale_min;
+float autocvar_g_sandbox_object_scale_max;
+float autocvar_g_sandbox_object_material_velocity_min;
+float autocvar_g_sandbox_object_material_velocity_factor;
index 0d95c453a1e6685b1d7032dd06dd520670ae628b..d2a21edc1002e0f27202b788ff2eea689e373bd5 100644 (file)
@@ -664,7 +664,7 @@ float CheatCommand(float argc)
        END_CHEAT_FUNCTION();
 }
 
-void crosshair_trace_plusvisibletriggers(entity pl);
+float Drag(entity e, float grab);
 void Drag_Begin(entity dragger, entity draggee, vector touchpoint);
 void Drag_Finish(entity dragger);
 float Drag_IsDraggable(entity draggee);
@@ -682,6 +682,70 @@ float CheatFrame()
 {
        BEGIN_CHEAT_FUNCTION();
 
+       // Dragging can be used as either a cheat, or a function for some objects. If sv_cheats is active,
+       // the cheat dragging is used (unlimited pickup range and any entity can be carried). If sv_cheats
+       // is disabled, normal dragging is used (limited pickup range and only dragable objects can be carried),
+       // grabbing itself no longer being accounted as cheating.
+
+       switch(0)
+       {
+               default:
+                       if(autocvar_sv_cheats)
+                       {
+                               // use cheat dragging if cheats are enabled
+                               IS_CHEAT(0, 0, CHRAME_DRAG);
+                               crosshair_trace_plusvisibletriggers(self);
+                               if(Drag(trace_ent, TRUE))
+                                       DID_CHEAT();
+                       }
+                       else
+                       {
+                               // drag is TRUE if the object can be picked up. While an object is being carried, the Drag() function
+                               // must execute for it either way, otherwise it would cause bugs if it went out of the player's trace.
+                               // This also makes sure that an object can only pe picked up if in range, but does not get dropped if
+                               // it goes out of range while slinging it around.
+
+                               float drag;
+                               makevectors(self.v_angle);
+                               WarpZone_TraceLine(self.origin + self.view_ofs, self.origin + self.view_ofs + v_forward * autocvar_g_grab_range, MOVE_NORMAL, self);
+                               switch(trace_ent.grab)
+                               {
+                                       case 0: // can't grab
+                                               break;
+                                       case 1: // owner can grab
+                                               if(trace_ent.owner == self || trace_ent.realowner == self)
+                                                       drag = TRUE;
+                                               break;
+                                       case 2: // owner and team mates can grab
+                                               if(!IsDifferentTeam(trace_ent.owner, self) || !IsDifferentTeam(trace_ent.realowner, self) || trace_ent.team == self.team)
+                                                       drag = TRUE;
+                                               break;
+                                       case 3: // anyone can grab
+                                               drag = TRUE;
+                                               break;
+                                       default:
+                                               break;
+                               }
+                               Drag(trace_ent, drag); // execute dragging
+                       }
+                       break;
+       }
+
+       END_CHEAT_FUNCTION();
+}
+
+
+
+
+
+// ENTITY DRAGGING
+
+float Drag(entity e, float pick)
+{
+       // returns TRUE when an entity has been picked up
+       // If pick is TRUE, the object can also be picked up if it's not being held already
+       // If pick is FALSE, only keep dragging the object if it's already being held
+
        if(Drag_IsDragging(self))
        {
                if(self.BUTTON_DRAG)
@@ -716,49 +780,21 @@ float CheatFrame()
        else
        {
                if(Drag_CanDrag(self))
-                       if(self.BUTTON_DRAG)
+                       if(self.BUTTON_DRAG && pick)
                        {
-                               crosshair_trace_plusvisibletriggers(self);
-                               if(trace_ent)
-                                       if(Drag_IsDraggable(trace_ent))
-                                               switch(0)
-                                               {
-                                                       default:
-                                                               IS_CHEAT(0, 0, CHRAME_DRAG);
-                                                               if(trace_ent.draggedby)
-                                                                       Drag_Finish(trace_ent.draggedby);
-                                                               if(trace_ent.tag_entity)
-                                                                       detach_sameorigin(trace_ent);
-                                                               Drag_Begin(self, trace_ent, trace_endpos);
-                                                               DID_CHEAT();
-                                                               break;
-                                               }
+                               if(e)
+                                       if(Drag_IsDraggable(e))
+                                       {
+                                                       if(e.draggedby)
+                                                               Drag_Finish(e.draggedby);
+                                                       if(e.tag_entity)
+                                                               detach_sameorigin(e);
+                                                       Drag_Begin(self, e, trace_endpos);
+                                                       return TRUE;
+                                       }
                        }
        }
-
-       END_CHEAT_FUNCTION();
-}
-
-
-
-
-
-// ENTITY DRAGGING
-
-void crosshair_trace_plusvisibletriggers(entity pl)
-{
-       entity first;
-       entity e;
-       first = findchainfloat(solid, SOLID_TRIGGER);
-
-       for (e = first; e; e = e.chain)
-               if (e.model != "")
-                       e.solid = SOLID_BSP;
-
-       crosshair_trace(pl);
-
-       for (e = first; e; e = e.chain)
-               e.solid = SOLID_TRIGGER;
+       return FALSE;
 }
 
 // on dragger:
@@ -829,6 +865,8 @@ void Drag_Finish(entity dragger)
 float Drag_IsDraggable(entity draggee)
 {
        // TODO add more checks for bad stuff here
+       if(draggee == world)
+               return FALSE;
        if(draggee.classname == "func_bobbing")
                return FALSE;
        if(draggee.classname == "door") // FIXME find out why these must be excluded, or work around the problem (trying to drag these causes like 4 fps)
@@ -937,11 +975,6 @@ void Drag_MoveDrag(entity from, entity to)
        }
 }
 
-
-
-
-
-
 void DragBox_Think()
 {
        if(self.aiment && self.enemy)
index 8c276919c51082206169b6050dd44335d7d1fc84..46152692e969e64dd9dfff6cd85f6e8b781747e1 100644 (file)
@@ -11,3 +11,5 @@ float CheatCommand(float argc);
 float CheatFrame();
 
 void Drag_MoveDrag(entity from, entity to); // call this from CopyBody
+
+float Drag(entity e, float grab); // used by sandbox code
\ No newline at end of file
index b84d03164d312d8c2b3b410bb8c86a602bb1fca0..86446f03678ffbb667d77dcb550fcd5926db7f42 100644 (file)
@@ -617,6 +617,8 @@ float client_cefc_accumulatortime;
 .float clip_size;
 .float minelayer_mines;
 
+.float grab; // 0 = can't grab, 1 = owner can grab, 2 = owner and team mates can grab, 3 = anyone can grab
+
 #define PROJECTILE_MAKETRIGGER(e) (e).solid = SOLID_CORPSE; (e).dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_BODY | DPCONTENTS_CORPSE
 // when doing this, hagar can go through clones
 // #define PROJECTILE_MAKETRIGGER(e) (e).solid = SOLID_BBOX
index 2fe562774d0d625468b36e4333f1e9547e17a607..2c089617277e583dbf44ab6914bcb6d9010b1431 100644 (file)
@@ -8,6 +8,21 @@ void crosshair_trace(entity pl)
 {
        traceline_antilag(pl, pl.cursor_trace_start, pl.cursor_trace_start + normalize(pl.cursor_trace_endpos - pl.cursor_trace_start) * MAX_SHOT_DISTANCE, MOVE_NORMAL, pl, ANTILAG_LATENCY(pl));
 }
+void crosshair_trace_plusvisibletriggers(entity pl)
+{
+       entity first;
+       entity e;
+       first = findchainfloat(solid, SOLID_TRIGGER);
+
+       for (e = first; e; e = e.chain)
+               if (e.model != "")
+                       e.solid = SOLID_BSP;
+
+       crosshair_trace(pl);
+
+       for (e = first; e; e = e.chain)
+               e.solid = SOLID_TRIGGER;
+}
 void WarpZone_traceline_antilag (entity source, vector v1, vector v2, float nomonst, entity forent, float lag);
 void WarpZone_crosshair_trace(entity pl)
 {
@@ -1086,6 +1101,8 @@ void readlevelcvars(void)
                MUTATOR_ADD(mutator_rocketflying);
        if(cvar("g_vampire"))
                MUTATOR_ADD(mutator_vampire);
+       if(cvar("g_sandbox"))
+               MUTATOR_ADD(sandbox);
 
        if(cvar("sv_allow_fullbright"))
                serverflags |= SERVERFLAG_ALLOW_FULLBRIGHT;
index 73b23bbd3f224201ecffa53725fe544a3b225573..621e4db6cb9baa5b1409b7a38da33e8d44cdb7bb 100644 (file)
@@ -188,3 +188,6 @@ MUTATOR_HOOKABLE(SV_ParseClientCommand);
                        return 0;
                }
        */
+
+MUTATOR_HOOKABLE(SV_StartFrame);
+       // runs globally each server frame
index 1ae28abe54ff961700c030d9e305119aa51bd5ec..349d945dfa87e6aac7b0ea6818e1864ac227e6ac 100644 (file)
@@ -7,3 +7,5 @@ MUTATOR_DECLARATION(mutator_nix);
 MUTATOR_DECLARATION(mutator_dodging);
 MUTATOR_DECLARATION(mutator_rocketflying);
 MUTATOR_DECLARATION(mutator_vampire);
+
+MUTATOR_DECLARATION(sandbox);
diff --git a/qcsrc/server/mutators/sandbox.qc b/qcsrc/server/mutators/sandbox.qc
new file mode 100644 (file)
index 0000000..6514dcc
--- /dev/null
@@ -0,0 +1,785 @@
+const float MAX_STORAGE_ATTACHMENTS = 16;
+float object_count;
+.float object_flood;
+.entity object_attach;
+.string material;
+
+.float touch_timer;
+void sandbox_ObjectFunction_Touch()
+{
+       // apply material impact effects
+
+       if(!self.material)
+               return;
+       if(self.touch_timer > time)
+               return; // don't execute each frame
+       self.touch_timer = time + 0.1;
+
+       // make particle count and sound volume depend on impact speed
+       float intensity;
+       intensity = vlen(self.velocity) + vlen(other.velocity);
+       if(intensity) // avoid divisions by 0
+               intensity /= 2; // average the two velocities
+       if not(intensity >= autocvar_g_sandbox_object_material_velocity_min)
+               return; // impact not strong enough to do anything
+       // now offset intensity and apply it to the effects
+       intensity -= autocvar_g_sandbox_object_material_velocity_min; // start from minimum velocity, not actual velocity
+       intensity = bound(0, intensity * autocvar_g_sandbox_object_material_velocity_factor, 1);
+
+       sound(self, CH_TRIGGER, strcat("object/impact_", self.material, "_", ftos(ceil(random() * 5)) , ".ogg"), VOL_BASE * intensity, ATTN_NORM);
+       pointparticles(particleeffectnum(strcat("impact_", self.material)), self.origin, '0 0 0', ceil(intensity * 10)); // allow a count from 1 to 10
+}
+
+void sandbox_ObjectFunction_Think()
+{
+       entity e;
+
+       // decide if and how this object can be grabbed
+       if(autocvar_g_sandbox_readonly)
+               self.grab = 0; // no grabbing
+       else if(autocvar_g_sandbox_editor_free < 2 && self.crypto_idfp)
+               self.grab = 1; // owner only
+       else
+               self.grab = 3; // anyone
+
+       // Object owner is stored via player UID, but we also need the owner as an entity (if the player is available on the server).
+       // Therefore, scan for all players, and update the owner as long as the player is present. We must always do this,
+       // since if the owning player disconnects, the object's owner should also be reset.
+       FOR_EACH_REALPLAYER(e) // bots can't have objects
+       {
+               if(self.crypto_idfp == e.crypto_idfp)
+               {
+                       self.realowner = e;
+                       break;
+               }
+               self.realowner = world;
+       }
+
+       self.nextthink = time;
+}
+
+entity sandbox_ObjectEdit_Get(float permissions)
+{
+       // returns the traced entity if the player can edit it, and world if not
+       // if permissions if FALSE, the object is returned regardless of editing rights
+       // attached objects are SOLID_NOT and don't risk getting traced
+
+       makevectors(self.v_angle);
+       WarpZone_TraceLine(self.origin + self.view_ofs, self.origin + self.view_ofs + v_forward * autocvar_g_sandbox_editor_distance_edit, MOVE_NORMAL, self);
+
+       if(trace_ent.classname != "object")
+               return world; // entity is not an object
+       if(!permissions)
+               return trace_ent; // don't check permissions, anyone can edit this object
+       if(!trace_ent.crypto_idfp)
+               return trace_ent; // the player who spawned this object did not have an UID, so anyone can edit it
+       if not(trace_ent.realowner != self && autocvar_g_sandbox_editor_free < 2)
+               return trace_ent; // object does not belong to the player, and players can only edit their own objects on this server
+       return world;
+}
+
+void sandbox_ObjectEdit_Scale(entity e, float f)
+{
+       e.scale = f;
+       if(e.scale)
+       {
+               e.scale = bound(autocvar_g_sandbox_object_scale_min, e.scale, autocvar_g_sandbox_object_scale_max);
+               setmodel(e, e.model); // reset mins and maxs based on mesh
+               setsize(e, e.mins * e.scale, e.maxs * e.scale); // adapt bounding box size to model size
+       }
+}
+
+.float old_movetype;
+void sandbox_ObjectAttach_Remove(entity e);
+void sandbox_ObjectAttach_Set(entity e, entity parent, string s)
+{
+       // attaches e to parent on string s
+
+       // we can't attach to an attachment, for obvious reasons
+       sandbox_ObjectAttach_Remove(e);
+
+       e.old_movetype = e.movetype; // persist physics
+       e.movetype = MOVETYPE_FOLLOW;
+       e.solid = SOLID_NOT;
+       e.takedamage = DAMAGE_NO;
+
+       setattachment(e, parent, s);
+       e.owner = parent;
+}
+
+void sandbox_ObjectAttach_Remove(entity e)
+{
+       // detaches any object attached to e
+
+       entity head;
+       for(head = world; (head = find(head, classname, "object")); )
+       {
+               if(head.owner == e)
+               {
+                       vector org;
+                       head.movetype = head.old_movetype; // restore persisted physics
+                       head.solid = SOLID_BBOX;
+                       head.takedamage = DAMAGE_AIM;
+
+                       org = gettaginfo(head, 0);
+                       setattachment(head, world, "");
+                       head.owner = world;
+
+                       // objects change origin and angles when detached, so apply previous position
+                       setorigin(head, org);
+                       head.angles = e.angles; // don't allow detached objects to spin or roll
+               }
+       }
+}
+
+entity sandbox_ObjectSpawn(float database)
+{
+       // spawn a new object with default properties
+
+       entity e;
+       e = spawn();
+       e.classname = "object";
+       e.takedamage = DAMAGE_AIM;
+       e.damageforcescale = 1;
+       e.solid = SOLID_BBOX; // SOLID_BSP would be best, but can lag the server badly
+       e.movetype = MOVETYPE_TOSS;
+       e.frame = 0;
+       e.skin = 0;
+       e.material = string_null;
+       e.touch = sandbox_ObjectFunction_Touch;
+       e.think = sandbox_ObjectFunction_Think;
+       e.nextthink = time;
+       //e.effects |= EF_SELECTABLE; // don't do this all the time, maybe just when editing objects?
+
+       if(!database)
+       {
+               // set the object's owner via player UID
+               // if the player does not have an UID, the owner cannot be stored and his objects may be edited by anyone
+               if(self.crypto_idfp != "")
+                       e.crypto_idfp = strzone(self.crypto_idfp);
+               else
+                       print_to(self, "^1SANDBOX - WARNING: ^7You spawned an object, but lack a player UID. ^1Your objects are not secured and can be edited by any player!");
+
+               // set public object information
+               e.netname = strzone(self.netname); // name of the owner
+               e.message = strzone(strftime(TRUE, "%d-%m-%Y %H:%M:%S")); // creation time
+               e.message2 = strzone(strftime(TRUE, "%d-%m-%Y %H:%M:%S")); // last editing time
+
+               // set origin and direction based on player position and view angle
+               makevectors(self.v_angle);
+               WarpZone_TraceLine(self.origin + self.view_ofs, self.origin + self.view_ofs + v_forward * autocvar_g_sandbox_editor_distance_spawn, MOVE_NORMAL, self);
+               setorigin(e, trace_endpos);
+               e.angles_y = self.v_angle_y;
+       }
+
+       object_count += 1;
+       return e;
+}
+
+void sandbox_ObjectRemove(entity e)
+{
+       sandbox_ObjectAttach_Remove(e); // detach child objects
+
+       if(e.material)  {       strunzone(e.material);  e.material = string_null;       }
+       if(e.crypto_idfp)       {       strunzone(e.crypto_idfp);       e.crypto_idfp = string_null;    }
+       if(e.netname)   {       strunzone(e.netname);   e.netname = string_null;        }
+       if(e.message)   {       strunzone(e.message);   e.message = string_null;        }
+       if(e.message2)  {       strunzone(e.message2);  e.message2 = string_null;       }
+       remove(e);
+       e = world;
+
+       object_count -= 1;
+}
+
+string port_string[MAX_STORAGE_ATTACHMENTS]; // fteqcc crashes if this isn't defined as a global
+
+string sandbox_ObjectPort_Save(entity e, float database)
+{
+       // save object properties, and return them as a string
+       float i;
+       string s;
+       entity head;
+
+       for(head = world; (head = find(head, classname, "object")); )
+       {
+               // the main object needs to be first in the array [0] with attached objects following
+               float slot, physics;
+               if(head == e) // this is the main object, place it first
+               {
+                       slot = 0;
+                       physics = head.movetype; // applied physics are normal physics for parents
+               }
+               else if(head.owner == e) // child object, list them in order
+               {
+                       i += 1; // children start from 1
+                       slot = i;
+                       physics = head.old_movetype; // persisted physics are normal physics for children
+                       gettaginfo(head.owner, head.tag_index); // get the name of the tag our object is attached to, used further below
+               }
+               else
+                       continue;
+
+               // ---------------- OBJECT PROPERTY STORAGE: SAVE ----------------
+               if(slot)
+               {
+                       // properties stored only for child objects
+                       if(gettaginfo_name)     port_string[slot] = strcat(port_string[slot], "\"", gettaginfo_name, "\" ");    else    port_string[slot] = strcat(port_string[slot], "- "); // none
+               }
+               else
+               {
+                       // properties stored only for parent objects
+                       if(database)
+                       {
+                               port_string[slot] = strcat(port_string[slot], sprintf("\"%.9v\"", head.origin), " ");
+                               port_string[slot] = strcat(port_string[slot], sprintf("\"%.9v\"", head.angles), " ");
+                       }
+               }
+               // properties stored for all objects
+               port_string[slot] = strcat(port_string[slot], "\"", head.model, "\" ");
+               port_string[slot] = strcat(port_string[slot], ftos(head.skin), " ");
+               port_string[slot] = strcat(port_string[slot], ftos(head.alpha), " ");
+               port_string[slot] = strcat(port_string[slot], sprintf("\"%.9v\"", head.colormod), " ");
+               port_string[slot] = strcat(port_string[slot], sprintf("\"%.9v\"", head.glowmod), " ");
+               port_string[slot] = strcat(port_string[slot], ftos(head.frame), " ");
+               port_string[slot] = strcat(port_string[slot], ftos(head.scale), " ");
+               port_string[slot] = strcat(port_string[slot], ftos(physics), " ");
+               port_string[slot] = strcat(port_string[slot], ftos(head.damageforcescale), " ");
+               if(head.material)       port_string[slot] = strcat(port_string[slot], "\"", head.material, "\" ");      else    port_string[slot] = strcat(port_string[slot], "- "); // none
+               if(database)
+               {
+                       // properties stored only for the database
+                       if(head.crypto_idfp)    port_string[slot] = strcat(port_string[slot], "\"", head.crypto_idfp, "\" ");   else    port_string[slot] = strcat(port_string[slot], "- "); // none
+                       port_string[slot] = strcat(port_string[slot], "\"", e.netname, "\" ");
+                       port_string[slot] = strcat(port_string[slot], "\"", e.message, "\" ");
+                       port_string[slot] = strcat(port_string[slot], "\"", e.message2, "\" ");
+               }
+       }
+
+       // now apply the array to a simple string, with the ; symbol separating objects
+       for(i = 0; i <= MAX_STORAGE_ATTACHMENTS; ++i)
+       {
+               if(port_string[i])
+                       s = strcat(s, port_string[i], "; ");
+               port_string[i] = string_null; // fully clear the string
+       }
+
+       return s;
+}
+
+entity sandbox_ObjectPort_Load(string s, float database)
+{
+       // load object properties, and spawn a new object with them
+       float n, i;
+       entity e, parent;
+
+       // separate objects between the ; symbols
+       n = tokenizebyseparator(s, "; ");
+       for(i = 0; i < n; ++i)
+               port_string[i] = argv(i);
+
+       // now separate and apply the properties of each object
+       for(i = 0; i < n; ++i)
+       {
+               float argv_num;
+               string tagname;
+               argv_num = 0;
+               tokenize_console(port_string[i]);
+               e = sandbox_ObjectSpawn(database);
+
+               // ---------------- OBJECT PROPERTY STORAGE: LOAD ----------------
+               if(i)
+               {
+                       // properties stored only for child objects
+                       if(argv(argv_num) != "-")       tagname = argv(argv_num);       else tagname = string_null;     ++argv_num;
+               }
+               else
+               {
+                       // properties stored only for parent objects
+                       if(database)
+                       {
+                               setorigin(e, stov(argv(argv_num)));     ++argv_num;
+                               e.angles = stov(argv(argv_num));        ++argv_num;
+                       }
+                       parent = e; // mark parent objects as such
+               }
+               // properties stored for all objects
+               setmodel(e, argv(argv_num));    ++argv_num;
+               e.skin = stof(argv(argv_num));  ++argv_num;
+               e.alpha = stof(argv(argv_num)); ++argv_num;
+               e.colormod = stov(argv(argv_num));      ++argv_num;
+               e.glowmod = stov(argv(argv_num));       ++argv_num;
+               e.frame = stof(argv(argv_num)); ++argv_num;
+               sandbox_ObjectEdit_Scale(e, stof(argv(argv_num)));      ++argv_num;
+               e.movetype = e.old_movetype = stof(argv(argv_num));     ++argv_num;
+               e.damageforcescale = stof(argv(argv_num));      ++argv_num;
+               if(e.material)  strunzone(e.material);  if(argv(argv_num) != "-")       e.material = strzone(argv(argv_num));   else    e.material = string_null;       ++argv_num;
+               if(database)
+               {
+                       // properties stored only for the database
+                       if(e.crypto_idfp)       strunzone(e.crypto_idfp);       if(argv(argv_num) != "-")       e.crypto_idfp = strzone(argv(argv_num));        else    e.crypto_idfp = string_null;    ++argv_num;
+                       if(e.netname)   strunzone(e.netname);   e.netname = strzone(argv(argv_num));    ++argv_num;
+                       if(e.message)   strunzone(e.message);   e.message = strzone(argv(argv_num));    ++argv_num;
+                       if(e.message2)  strunzone(e.message2);  e.message2 = strzone(argv(argv_num));   ++argv_num;
+               }
+
+               // attach last
+               if(i)
+                       sandbox_ObjectAttach_Set(e, parent, tagname);
+       }
+
+       for(i = 0; i <= MAX_STORAGE_ATTACHMENTS; ++i)
+               port_string[i] = string_null; // fully clear the string
+
+       return e;
+}
+
+void sandbox_Database_Save()
+{
+       // saves all objects to the database file
+       entity head;
+       string file_name;
+       float file_get;
+
+       file_name = strcat("sandbox/storage_", autocvar_g_sandbox_storage_name, "_", GetMapname(), ".txt");
+       file_get = fopen(file_name, FILE_WRITE);
+       fputs(file_get, strcat("// sandbox storage \"", autocvar_g_sandbox_storage_name, "\" for map \"", GetMapname(), "\" last updated ", strftime(TRUE, "%d-%m-%Y %H:%M:%S")));
+       fputs(file_get, strcat(" containing ", ftos(object_count), " objects\n"));
+
+       for(head = world; (head = find(head, classname, "object")); )
+       {
+               // attached objects are persisted separately, ignore them here
+               if(head.owner != world)
+                       continue;
+
+               // use a line of text for each object, listing all properties
+               fputs(file_get, strcat(sandbox_ObjectPort_Save(head, TRUE), "\n"));
+       }
+       fclose(file_get);
+}
+
+void sandbox_Database_Load()
+{
+       // loads all objects from the database file
+       string file_read, file_name;
+       float file_get, i;
+
+       file_name = strcat("sandbox/storage_", autocvar_g_sandbox_storage_name, "_", GetMapname(), ".txt");
+       file_get = fopen(file_name, FILE_READ);
+       if(file_get < 0)
+       {
+               if(autocvar_g_sandbox_info > 0)
+                       print(strcat("^3SANDBOX - SERVER: ^7could not find storage file ^3", file_name, "^7, no objects were loaded\n"));
+       }
+       else
+       {
+               for(;;)
+               {
+                       file_read = fgets(file_get);
+                       if(!file_read)
+                               break;
+                       if(substring(file_read, 0, 2) == "//")
+                               continue;
+                       if(substring(file_read, 0, 1) == "#")
+                               continue;
+
+                       entity e;
+                       e = sandbox_ObjectPort_Load(file_read, TRUE);
+
+                       if(e.material)
+                       {
+                               // since objects are being loaded for the first time, precache material sounds for each
+                               for (i = 1; i <= 5; i++) // 5 sounds in total
+                                       precache_sound(strcat("object/impact_", e.material, "_", ftos(i), ".ogg"));
+                       }
+               }
+               if(autocvar_g_sandbox_info > 0)
+                       print(strcat("^3SANDBOX - SERVER: ^7successfully loaded storage file ^3", file_name, "\n"));
+       }
+       fclose(file_get);
+}
+
+MUTATOR_HOOKFUNCTION(sandbox_PlayerCommand)
+{
+       if(MUTATOR_RETURNVALUE) // command was already handled?
+               return FALSE;
+       if(cmd_name == "g_sandbox")
+       {
+               if(autocvar_g_sandbox_readonly)
+               {
+                       print_to(self, "^2SANDBOX - INFO: ^7Sandbox mode is active, but in read-only mode. Sandbox commands cannot be used");
+                       return TRUE;
+               }
+               if(cmd_argc < 2)
+               {
+                       print_to(self, "^2SANDBOX - INFO: ^7Sandbox mode is active. For usage information, type 'sandbox help'");
+                       return TRUE;
+               }
+
+               switch(argv(1))
+               {
+                       entity e;
+                       float i;
+                       string s;
+
+                       // ---------------- COMMAND: HELP ----------------
+                       case "help":
+                               print_to(self, "You can use the following sandbox commands:");
+                               print_to(self, "^7\"^2object_spawn ^3models/foo/bar.md3^7\" spawns a new object in front of the player, and gives it the specified model");
+                               print_to(self, "^7\"^2object_remove^7\" removes the object the player is looking at. Players can only remove their own objects");
+                               print_to(self, "^7\"^2object_duplicate ^3value^7\" duplicates the object, if the player has copying rights over the original");
+                               print_to(self, "^3copy value ^7- copies the properties of the object to the specified client cvar");
+                               print_to(self, "^3paste value ^7- spawns an object with the given properties. Properties or cvars must be specified as follows; eg1: \"0 1 2 ...\", eg2: \"$cl_cvar\"");
+                               print_to(self, "^7\"^2object_attach ^3property value^7\" attaches one object to another. Players can only attach their own objects");
+                               print_to(self, "^3get ^7- selects the object you are facing as the object to be attached");
+                               print_to(self, "^3set value ^7- attaches the previously selected object to the object you are facing, on the specified bone");
+                               print_to(self, "^3remove ^7- detaches all objects from the object you are facing");
+                               print_to(self, "^7\"^2object_edit ^3property value^7\" edits the given property of the object. Players can only edit their own objects");
+                               print_to(self, "^3skin value ^7- changes the skin of the object");
+                               print_to(self, "^3alpha value ^7- sets object transparency");
+                               print_to(self, "^3colormod \"value_x value_y value_z\" ^7- main object color");
+                               print_to(self, "^3glowmod \"value_x value_y value_z\" ^7- glow object color");
+                               print_to(self, "^3frame value ^7- object animation frame, for self-animated models");
+                               print_to(self, "^3scale value ^7- changes object scale. 0.5 is half size and 2 is double size");
+                               print_to(self, "^3physics value ^7- object physics, 0 = static, 1 = movable, 2 = physical");
+                               print_to(self, "^3force value ^7- amount of force applied to objects that are shot");
+                               print_to(self, "^3material value ^7- sets the material of the object. Default materials are: metal, stone, wood, flesh");
+                               print_to(self, "^7\"^2object_claim^7\" sets the player as the owner of the object, if he has the right to edit it");
+                               print_to(self, "^7\"^2object_info ^3value^7\" shows public information about the object");
+                               print_to(self, "^3object ^7- prints general information about the object, such as owner and creation / editing date");
+                               print_to(self, "^3mesh ^7- prints information about the object's mesh, including skeletal bones");
+                               print_to(self, "^3attachments ^7- prints information about the object's attachments");
+                               print_to(self, "^7The ^1drag object ^7key can be used to grab and carry objects. Players can only grab their own objects");
+                               return TRUE;
+
+                       // ---------------- COMMAND: OBJECT, SPAWN ----------------
+                       case "object_spawn":
+                               if(time < self.object_flood)
+                               {
+                                       print_to(self, strcat("^1SANDBOX - WARNING: ^7Flood protection active. Please wait ^3", ftos(self.object_flood - time), " ^7seconds beofore spawning another object"));
+                                       return TRUE;
+                               }
+                               self.object_flood = time + autocvar_g_sandbox_editor_flood;
+                               if(object_count >= autocvar_g_sandbox_editor_maxobjects)
+                               {
+                                       print_to(self, strcat("^1SANDBOX - WARNING: ^7Cannot spawn any more objects. Up to ^3", ftos(autocvar_g_sandbox_editor_maxobjects), " ^7objects may exist at a time"));
+                                       return TRUE;
+                               }
+                               if(cmd_argc < 3)
+                               {
+                                       print_to(self, "^1SANDBOX - WARNING: ^7Attempted to spawn an object without specifying a model. Please specify the path to your model file after the 'object_spawn' command");
+                                       return TRUE;
+                               }
+                               if not(fexists(argv(2)))
+                               {
+                                       print_to(self, "^1SANDBOX - WARNING: ^7Attempted to spawn an object with a non-existent model. Make sure the path to your model file is correct");
+                                       return TRUE;
+                               }
+
+                               e = sandbox_ObjectSpawn(FALSE);
+                               setmodel(e, argv(2));
+
+                               if(autocvar_g_sandbox_info > 0)
+                                       print(strcat("^3SANDBOX - SERVER: ^7", self.netname, " spawned an object at origin ^3", vtos(e.origin), "\n"));
+                               return TRUE;
+
+                       // ---------------- COMMAND: OBJECT, REMOVE ----------------
+                       case "object_remove":
+                               e = sandbox_ObjectEdit_Get(TRUE);
+                               if(e != world)
+                               {
+                                       if(autocvar_g_sandbox_info > 0)
+                                               print(strcat("^3SANDBOX - SERVER: ^7", self.netname, " removed an object at origin ^3", vtos(e.origin), "\n"));
+                                       sandbox_ObjectRemove(e);
+                                       return TRUE;
+                               }
+
+                               print_to(self, "^1SANDBOX - WARNING: ^7Object could not be removed. Make sure you are facing an object that you have edit rights over");
+                               return TRUE;
+
+                       // ---------------- COMMAND: OBJECT, DUPLICATE ----------------
+                       case "object_duplicate":
+                               switch(argv(2))
+                               {
+                                       case "copy":
+                                               // copies customizable properties of the selected object to the clipboard cvar
+                                               e = sandbox_ObjectEdit_Get(autocvar_g_sandbox_editor_free); // can we copy objects we can't edit?
+                                               if(e != world)
+                                               {
+                                                       s = sandbox_ObjectPort_Save(e, FALSE);
+                                                       s = strreplace("\"", "\\\"", s);
+                                                       stuffcmd(self, strcat("set ", argv(3), " \"", s, "\""));
+
+                                                       print_to(self, "^2SANDBOX - INFO: ^7Object copied to clipboard");
+                                                       return TRUE;
+                                               }
+                                               print_to(self, "^1SANDBOX - WARNING: ^7Object could not be copied. Make sure you are facing an object that you have copy rights over");
+                                               return TRUE;
+
+                                       case "paste":
+                                               // spawns a new object using the properties in the player's clipboard cvar
+                                               if(time < self.object_flood)
+                                               {
+                                                       print_to(self, strcat("^1SANDBOX - WARNING: ^7Flood protection active. Please wait ^3", ftos(self.object_flood - time), " ^7seconds beofore spawning another object"));
+                                                       return TRUE;
+                                               }
+                                               self.object_flood = time + autocvar_g_sandbox_editor_flood;
+                                               if(!argv(3)) // no object in clipboard
+                                               {
+                                                       print_to(self, "^1SANDBOX - WARNING: ^7No object in clipboard. You must copy an object before you can paste it");
+                                                       return TRUE;
+                                               }
+                                               if(object_count >= autocvar_g_sandbox_editor_maxobjects)
+                                               {
+                                                       print_to(self, strcat("^1SANDBOX - WARNING: ^7Cannot spawn any more objects. Up to ^3", ftos(autocvar_g_sandbox_editor_maxobjects), " ^7objects may exist at a time"));
+                                                       return TRUE;
+                                               }
+                                               e = sandbox_ObjectPort_Load(argv(3), FALSE);
+
+                                               print_to(self, "^2SANDBOX - INFO: ^7Object pasted successfully");
+                                               if(autocvar_g_sandbox_info > 0)
+                                                       print(strcat("^3SANDBOX - SERVER: ^7", self.netname, " pasted an object at origin ^3", vtos(e.origin), "\n"));
+                                               return TRUE;
+                               }
+                               return TRUE;
+
+                       // ---------------- COMMAND: OBJECT, ATTACH ----------------
+                       case "object_attach":
+                               switch(argv(2))
+                               {
+                                       case "get":
+                                               // select e as the object as meant to be attached
+                                               e = sandbox_ObjectEdit_Get(TRUE);
+                                               if(e != world)
+                                               {
+                                                       self.object_attach = e;
+                                                       print_to(self, "^2SANDBOX - INFO: ^7Object selected for attachment");
+                                                       return TRUE;
+                                               }
+                                               print_to(self, "^1SANDBOX - WARNING: ^7Object could not be selected for attachment. Make sure you are facing an object that you have edit rights over");
+                                               return TRUE;
+                                       case "set":
+                                               if(self.object_attach == world)
+                                               {
+                                                       print_to(self, "^1SANDBOX - WARNING: ^7No object selected for attachment. Please select an object to be attached first.");
+                                                       return TRUE;
+                                               }
+
+                                               // attaches the previously selected object to e
+                                               e = sandbox_ObjectEdit_Get(TRUE);
+                                               if(e != world)
+                                               {
+                                                       sandbox_ObjectAttach_Set(self.object_attach, e, argv(3));
+                                                       self.object_attach = world; // object was attached, no longer keep it scheduled for attachment
+                                                       print_to(self, "^2SANDBOX - INFO: ^7Object attached successfully");
+                                                       if(autocvar_g_sandbox_info > 1)
+                                                               print(strcat("^3SANDBOX - SERVER: ^7", self.netname, " attached objects at origin ^3", vtos(e.origin), "\n"));
+                                                       return TRUE;
+                                               }
+                                               print_to(self, "^1SANDBOX - WARNING: ^7Object could not be attached to the parent. Make sure you are facing an object that you have edit rights over");
+                                               return TRUE;
+                                       case "remove":
+                                               // removes e if it was attached
+                                               e = sandbox_ObjectEdit_Get(TRUE);
+                                               if(e != world)
+                                               {
+                                                       sandbox_ObjectAttach_Remove(e);
+                                                       print_to(self, "^2SANDBOX - INFO: ^7Child objects detached successfully");
+                                                       if(autocvar_g_sandbox_info > 1)
+                                                               print(strcat("^3SANDBOX - SERVER: ^7", self.netname, " detached objects at origin ^3", vtos(e.origin), "\n"));
+                                                       return TRUE;
+                                               }
+                                               print_to(self, "^1SANDBOX - WARNING: ^7Child objects could not be detached. Make sure you are facing an object that you have edit rights over");
+                                               return TRUE;
+                               }
+                               return TRUE;
+
+                       // ---------------- COMMAND: OBJECT, EDIT ----------------
+                       case "object_edit":
+                               if(!argv(2))
+                               {
+                                       print_to(self, "^1SANDBOX - WARNING: ^7Too few parameters. You must specify a property to edit");
+                                       return TRUE;
+                               }
+
+                               e = sandbox_ObjectEdit_Get(TRUE);
+                               if(e != world)
+                               {
+                                       switch(argv(2))
+                                       {
+                                               case "skin":
+                                                       e.skin = stof(argv(3));
+                                                       break;
+                                               case "alpha":
+                                                       e.alpha = stof(argv(3));
+                                                       break;
+                                               case "color_main":
+                                                       e.colormod = stov(argv(3));
+                                                       break;
+                                               case "color_glow":
+                                                       e.glowmod = stov(argv(3));
+                                                       break;
+                                               case "frame":
+                                                       e.frame = stof(argv(3));
+                                                       break;
+                                               case "scale":
+                                                       sandbox_ObjectEdit_Scale(e, stof(argv(3)));
+                                                       break;
+                                               case "physics":
+                                                       switch(argv(3))
+                                                       {
+                                                               case "0": // static
+                                                                       e.movetype = MOVETYPE_NONE;
+                                                                       break;
+                                                               case "1": // movable
+                                                                       e.movetype = MOVETYPE_TOSS;
+                                                                       break;
+                                                               case "2": // physical
+                                                                       e.movetype = MOVETYPE_PHYSICS;
+                                                                       break;
+                                                               default:
+                                                                       break;
+                                                       }
+                                                       break;
+                                               case "force":
+                                                       e.damageforcescale = stof(argv(3));
+                                                       break;
+                                               case "material":
+                                                       if(e.material)  strunzone(e.material);
+                                                       if(argv(3))
+                                                       {
+                                                               for (i = 1; i <= 5; i++) // precache material sounds, 5 in total
+                                                                       precache_sound(strcat("object/impact_", argv(3), "_", ftos(i), ".ogg"));
+                                                               e.material = strzone(argv(3));
+                                                       }
+                                                       else
+                                                               e.material = string_null; // no material
+                                                       break;
+                                               default:
+                                                       print_to(self, "^1SANDBOX - WARNING: ^7Invalid object property. For usage information, type 'sandbox help'");
+                                                       return TRUE;
+                                       }
+
+                                       // update last editing time
+                                       if(e.message2)  strunzone(e.message2);
+                                       e.message2 = strzone(strftime(TRUE, "%d-%m-%Y %H:%M:%S"));
+
+                                       if(autocvar_g_sandbox_info > 1)
+                                               print(strcat("^3SANDBOX - SERVER: ^7", self.netname, " edited property ^3", argv(2), " ^7of an object at origin ^3", vtos(e.origin), "\n"));
+                                       return TRUE;
+                               }
+
+                               print_to(self, "^1SANDBOX - WARNING: ^7Object could not be edited. Make sure you are facing an object that you have edit rights over");
+                               return TRUE;
+
+                       // ---------------- COMMAND: OBJECT, CLAIM ----------------
+                       case "object_claim":
+                               // if the player can edit an object but is not its owner, this can be used to claim that object
+                               if(self.crypto_idfp == "")
+                               {
+                                       print_to(self, "^1SANDBOX - WARNING: ^7You do not have a player UID, and cannot claim objects");
+                                       return TRUE;
+                               }
+                               e = sandbox_ObjectEdit_Get(TRUE);
+                               if(e != world)
+                               {
+                                       // update the owner's name
+                                       // Do this before checking if you're already the owner and skipping if such, so we
+                                       // also update the player's nickname if he changed it (but has the same player UID)
+                                       if(e.netname != self.netname)
+                                       {
+                                               if(e.netname)   strunzone(e.netname);
+                                               e.netname = strzone(self.netname);
+                                               print_to(self, "^2SANDBOX - INFO: ^7Object owner name updated");
+                                       }
+
+                                       if(e.crypto_idfp == self.crypto_idfp)
+                                       {
+                                               print_to(self, "^2SANDBOX - INFO: ^7Object is already yours, nothing to claim");
+                                               return TRUE;
+                                       }
+
+                                       if(e.crypto_idfp)       strunzone(e.crypto_idfp);
+                                       e.crypto_idfp = strzone(self.crypto_idfp);
+
+                                       print_to(self, "^2SANDBOX - INFO: ^7Object claimed successfully");
+                               }
+                               print_to(self, "^1SANDBOX - WARNING: ^7Object could not be claimed. Make sure you are facing an object that you have edit rights over");
+                               return TRUE;
+
+                       // ---------------- COMMAND: OBJECT, INFO ----------------
+                       case "object_info":
+                               // prints public information about the object to the player
+                               e = sandbox_ObjectEdit_Get(FALSE);
+                               if(e != world)
+                               {
+                                       switch(argv(2))
+                                       {
+                                               case "object":
+                                                       print_to(self, strcat("^2SANDBOX - INFO: ^7Object is owned by \"^7", e.netname, "^7\", created \"^3", e.message, "^7\", last edited \"^3", e.message2, "^7\""));
+                                                       return TRUE;
+                                               case "mesh":
+                                                       for(i = 1; gettaginfo(e, i); i++)
+                                                               s = strcat(s, "^7\"^5", gettaginfo_name, "^7\", ");
+                                                       print_to(self, strcat("^2SANDBOX - INFO: ^7Object mesh is \"^3", e.model, "^7\" at animation frame ^3", ftos(e.frame), " ^7containing the following tags: ", s));
+                                                       return TRUE;
+                                               case "attachments":
+                                                       // this should show the same info as 'mesh' but for attachments
+                                                       entity head;
+                                                       for(head = world; (head = find(head, classname, "object")); )
+                                                       {
+                                                               if(head.owner == e)
+                                                               {
+                                                                       ++i; // start from 1
+                                                                       gettaginfo(e, head.tag_index);
+                                                                       s = strcat(s, "^1attachment ", ftos(i), "^7 has mesh \"^3", head.model, "^7\" at animation frame ^3", ftos(head.frame));
+                                                                       s = strcat(s, "^7 and is attached to bone \"^5", gettaginfo_name, "^7\", ");
+                                                               }
+                                                       }
+                                                       if(i) // object contains attachments
+                                                               print_to(self, strcat("^2SANDBOX - INFO: ^7Object contains the following ^1", ftos(i), "^7 attachment(s): ", s));
+                                                       else
+                                                               print_to(self, "^2SANDBOX - INFO: ^7Object contains no attachments");
+                                                       return TRUE;
+                                       }
+                               }
+                               print_to(self, "^1SANDBOX - WARNING: ^7No information could be found. Make sure you are facing an object");
+                               return TRUE;
+
+                       // ---------------- COMMAND: DEFAULT ----------------
+                       default:
+                               print_to(self, "Invalid command. For usage information, type 'sandbox help'");
+                               return TRUE;
+               }
+       }
+       return FALSE;
+}
+
+float autosave_time;
+MUTATOR_HOOKFUNCTION(sandbox_StartFrame)
+{
+       if(!autocvar_g_sandbox_storage_autosave)
+               return FALSE;
+       if(time < autosave_time)
+               return FALSE;
+       autosave_time = time + autocvar_g_sandbox_storage_autosave;
+
+       sandbox_Database_Save();
+
+       return TRUE;
+}
+
+MUTATOR_DEFINITION(sandbox)
+{
+       MUTATOR_HOOK(SV_ParseClientCommand, sandbox_PlayerCommand, CBC_ORDER_ANY);
+       MUTATOR_HOOK(SV_StartFrame, sandbox_StartFrame, CBC_ORDER_ANY);
+
+       MUTATOR_ONADD
+       {
+               autosave_time = time + autocvar_g_sandbox_storage_autosave; // don't save the first server frame
+               if(autocvar_g_sandbox_storage_autoload)
+                       sandbox_Database_Load();
+       }
+
+       return FALSE;
+}
+
index c29ac5ff965380bd8dde021b95856a8eef9a385d..8ba264665d93c74a002f72027e21c1287e88f9b9 100644 (file)
@@ -197,6 +197,7 @@ mutators/mutator_nix.qc
 mutators/mutator_dodging.qc
 mutators/mutator_rocketflying.qc
 mutators/mutator_vampire.qc
+mutators/sandbox.qc
 
 ../warpzonelib/anglestransform.qc
 ../warpzonelib/mathlib.qc
index eb386005d16f16201ba064b01d32e799b1a85bee..d11f235224056a9c9526cbaa5944e2b514a5bc89 100644 (file)
@@ -242,6 +242,8 @@ void StartFrame (void)
 
        FOR_EACH_PLAYER(self)
                self.porto_forbidden = max(0, self.porto_forbidden - 1);
+
+       MUTATOR_CALLHOOK(SV_StartFrame);
 }
 
 .vector originjitter;
diff --git a/sound/object/impact_flesh_1.ogg b/sound/object/impact_flesh_1.ogg
new file mode 100644 (file)
index 0000000..2d7d423
Binary files /dev/null and b/sound/object/impact_flesh_1.ogg differ
diff --git a/sound/object/impact_flesh_2.ogg b/sound/object/impact_flesh_2.ogg
new file mode 100644 (file)
index 0000000..c490c75
Binary files /dev/null and b/sound/object/impact_flesh_2.ogg differ
diff --git a/sound/object/impact_flesh_3.ogg b/sound/object/impact_flesh_3.ogg
new file mode 100644 (file)
index 0000000..12af028
Binary files /dev/null and b/sound/object/impact_flesh_3.ogg differ
diff --git a/sound/object/impact_flesh_4.ogg b/sound/object/impact_flesh_4.ogg
new file mode 100644 (file)
index 0000000..18bf012
Binary files /dev/null and b/sound/object/impact_flesh_4.ogg differ
diff --git a/sound/object/impact_flesh_5.ogg b/sound/object/impact_flesh_5.ogg
new file mode 100644 (file)
index 0000000..2c83777
Binary files /dev/null and b/sound/object/impact_flesh_5.ogg differ
diff --git a/sound/object/impact_metal_1.ogg b/sound/object/impact_metal_1.ogg
new file mode 100644 (file)
index 0000000..331728a
Binary files /dev/null and b/sound/object/impact_metal_1.ogg differ
diff --git a/sound/object/impact_metal_2.ogg b/sound/object/impact_metal_2.ogg
new file mode 100644 (file)
index 0000000..645dc20
Binary files /dev/null and b/sound/object/impact_metal_2.ogg differ
diff --git a/sound/object/impact_metal_3.ogg b/sound/object/impact_metal_3.ogg
new file mode 100644 (file)
index 0000000..d1128fd
Binary files /dev/null and b/sound/object/impact_metal_3.ogg differ
diff --git a/sound/object/impact_metal_4.ogg b/sound/object/impact_metal_4.ogg
new file mode 100644 (file)
index 0000000..cbb99f8
Binary files /dev/null and b/sound/object/impact_metal_4.ogg differ
diff --git a/sound/object/impact_metal_5.ogg b/sound/object/impact_metal_5.ogg
new file mode 100644 (file)
index 0000000..60d4d43
Binary files /dev/null and b/sound/object/impact_metal_5.ogg differ
diff --git a/sound/object/impact_stone_1.ogg b/sound/object/impact_stone_1.ogg
new file mode 100644 (file)
index 0000000..429b1c5
Binary files /dev/null and b/sound/object/impact_stone_1.ogg differ
diff --git a/sound/object/impact_stone_2.ogg b/sound/object/impact_stone_2.ogg
new file mode 100644 (file)
index 0000000..0a4feb7
Binary files /dev/null and b/sound/object/impact_stone_2.ogg differ
diff --git a/sound/object/impact_stone_3.ogg b/sound/object/impact_stone_3.ogg
new file mode 100644 (file)
index 0000000..f33b43c
Binary files /dev/null and b/sound/object/impact_stone_3.ogg differ
diff --git a/sound/object/impact_stone_4.ogg b/sound/object/impact_stone_4.ogg
new file mode 100644 (file)
index 0000000..507e4a6
Binary files /dev/null and b/sound/object/impact_stone_4.ogg differ
diff --git a/sound/object/impact_stone_5.ogg b/sound/object/impact_stone_5.ogg
new file mode 100644 (file)
index 0000000..89c9df7
Binary files /dev/null and b/sound/object/impact_stone_5.ogg differ
diff --git a/sound/object/impact_wood_1.ogg b/sound/object/impact_wood_1.ogg
new file mode 100644 (file)
index 0000000..cb665e0
Binary files /dev/null and b/sound/object/impact_wood_1.ogg differ
diff --git a/sound/object/impact_wood_2.ogg b/sound/object/impact_wood_2.ogg
new file mode 100644 (file)
index 0000000..ab826a0
Binary files /dev/null and b/sound/object/impact_wood_2.ogg differ
diff --git a/sound/object/impact_wood_3.ogg b/sound/object/impact_wood_3.ogg
new file mode 100644 (file)
index 0000000..1d383c2
Binary files /dev/null and b/sound/object/impact_wood_3.ogg differ
diff --git a/sound/object/impact_wood_4.ogg b/sound/object/impact_wood_4.ogg
new file mode 100644 (file)
index 0000000..314917d
Binary files /dev/null and b/sound/object/impact_wood_4.ogg differ
diff --git a/sound/object/impact_wood_5.ogg b/sound/object/impact_wood_5.ogg
new file mode 100644 (file)
index 0000000..9d8c929
Binary files /dev/null and b/sound/object/impact_wood_5.ogg differ
index 8f2ddab6c9ad842b1d5578a6b1d4c9b48abadb1e..fba2a0ab2c0670513ae2566cb3018a4278a77607 100644 (file)
@@ -38,6 +38,7 @@ mand1nga
 Merlijn Hofstra
 remaxim
 Stephan
+Independent.nu
 
 *Engine Code Additions & QA
 Rudolf "divVerent" Polzer